tksbrokerapi.TKSBrokerAPI

TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios, as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: from the console, it has a rich keys and commands, or you can use it as Python module with python import.

TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.

   1# -*- coding: utf-8 -*-
   2# Author: Timur Gilmullin
   3
   4"""
   5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios,
   6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
   7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`.
   8
   9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive
  10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
  11
  12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH
  13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
  14- **See CLI examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
  15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
  16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/
  17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/
  18"""
  19
  20# Copyright (c) 2022 Gilmillin Timur Mansurovich
  21#
  22# Licensed under the Apache License, Version 2.0 (the "License");
  23# you may not use this file except in compliance with the License.
  24# You may obtain a copy of the License at
  25#
  26#     http://www.apache.org/licenses/LICENSE-2.0
  27#
  28# Unless required by applicable law or agreed to in writing, software
  29# distributed under the License is distributed on an "AS IS" BASIS,
  30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  31# See the License for the specific language governing permissions and
  32# limitations under the License.
  33
  34
  35import sys
  36import os
  37from argparse import ArgumentParser
  38from importlib.metadata import version
  39
  40from dateutil.tz import tzlocal
  41from time import sleep
  42
  43import re
  44import json
  45import requests
  46import traceback as tb
  47from typing import Union
  48
  49from multiprocessing import cpu_count, Lock
  50from multiprocessing.pool import ThreadPool
  51import pandas as pd
  52
  53from mako.template import Template  # Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
  54from Templates import *  # Some html-templates used by reporting methods in TKSBrokerAPI module
  55from TKSEnums import *  # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/
  56from TradeRoutines import *  # This library contains some methods used by trade scenarios implemented with TKSBrokerAPI module
  57
  58from pricegenerator.PriceGenerator import PriceGenerator, uLogger  # This module has a lot of instruments to work with candles data (https://github.com/Tim55667757/PriceGenerator)
  59from pricegenerator.UniLogger import DisableLogger as PGDisLog  # Method for disable log from PriceGenerator
  60
  61import UniLogger as uLog  # Logger for TKSBrokerAPI
  62
  63
  64# --- Common technical parameters:
  65
  66PGDisLog(uLogger.handlers[0])  # Disable 3-rd party logging from PriceGenerator
  67uLogger = uLog.UniLogger  # init logger for TKSBrokerAPI
  68uLogger.level = 10  # debug level by default for TKSBrokerAPI module
  69uLogger.handlers[0].level = 20  # info level by default for STDOUT of TKSBrokerAPI module
  70
  71__version__ = "1.6"  # The "major.minor" version setup here, but build number define at the build-server only
  72
  73CPU_COUNT = cpu_count()  # host's real CPU count
  74CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1  # how many CPUs will be used for parallel calculations
  75
  76
  77class TinkoffBrokerServer:
  78    """
  79    This class implements methods to work with Tinkoff broker server.
  80
  81    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  82
  83    About `token`: https://tinkoff.github.io/investAPI/token/
  84    """
  85    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  86        """
  87        Main class init.
  88
  89        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  90        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  91                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  92        :param useCache: use default cache file with raw data to use instead of `iList`.
  93                         True by default. Cache is auto-update if new day has come.
  94                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  95        :param defaultCache: path to default cache file. `dump.json` by default.
  96        """
  97        if token is None or not token:
  98            try:
  99                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 100                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 101
 102            except KeyError:
 103                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 104                raise Exception("Token required")
 105
 106        else:
 107            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 108            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 109
 110        if accountId is None or not accountId:
 111            try:
 112                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 113                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 114
 115            except KeyError:
 116                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 117
 118        else:
 119            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 120            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 121
 122        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 123        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 124
 125        Latest version: https://pypi.org/project/tksbrokerapi/
 126        """
 127
 128        self.__lock = Lock()  # initialize multiprocessing mutex lock
 129
 130        self.aliases = TKS_TICKER_ALIASES
 131        """Some aliases instead official tickers.
 132
 133        See also: `TKSEnums.TKS_TICKER_ALIASES`
 134        """
 135
 136        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 137
 138        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 139
 140        self._ticker = ""
 141        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 142
 143        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 144        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 145
 146        See also: `SearchByTicker()`, `SearchInstruments()`.
 147        """
 148
 149        self._figi = ""
 150        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 151
 152        See also: `SearchByFIGI()`, `SearchInstruments()`.
 153        """
 154
 155        self.depth = 1
 156        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 157
 158        See also: `GetCurrentPrices()`.
 159        """
 160
 161        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 162        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 163
 164        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 165        """
 166
 167        uLogger.debug("Broker API server: {}".format(self.server))
 168
 169        self.timeout = 15
 170        """Server operations timeout in seconds. Default: `15`.
 171
 172        See also: `SendAPIRequest()`.
 173        """
 174
 175        self.headers = {
 176            "Content-Type": "application/json",
 177            "accept": "application/json",
 178            "Authorization": "Bearer {}".format(self.token),
 179            "x-app-name": "Tim55667757.TKSBrokerAPI",
 180        }
 181        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 182
 183        See also: `SendAPIRequest()`.
 184        """
 185
 186        self.body = None
 187        """Request body which send to broker server. Default: `None`.
 188
 189        See also: `SendAPIRequest()`.
 190        """
 191
 192        self.moreDebug = False
 193        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 194
 195        self.useHTMLReports = False
 196        """
 197        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 198        
 199        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 200        """
 201
 202        self.historyFile = None
 203        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 204
 205        See also: `History()`.
 206        """
 207
 208        self.htmlHistoryFile = "index.html"
 209        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 210
 211        See also: `ShowHistoryChart()`.
 212        """
 213
 214        self.instrumentsFile = "instruments.md"
 215        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 216
 217        See also: `ShowInstrumentsInfo()`.
 218        """
 219
 220        self.searchResultsFile = "search-results.md"
 221        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 222
 223        See also: `SearchInstruments()`.
 224        """
 225
 226        self.pricesFile = "prices.md"
 227        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 228
 229        See also: `GetListOfPrices()`.
 230        """
 231
 232        self.infoFile = "info.md"
 233        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 234
 235        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 236        """
 237
 238        self.bondsXLSXFile = "ext-bonds.xlsx"
 239        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 240        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 241
 242        See also: `ExtendBondsData()`.
 243        """
 244
 245        self.calendarFile = "calendar.md"
 246        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 247        
 248        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 249
 250        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 251        """
 252
 253        self.overviewFile = "overview.md"
 254        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 255
 256        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 257        """
 258
 259        self.overviewDigestFile = "overview-digest.md"
 260        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 261
 262        See also: `Overview()` with parameter `details="digest"`.
 263        """
 264
 265        self.overviewPositionsFile = "overview-positions.md"
 266        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 267
 268        See also: `Overview()` with parameter `details="positions"`.
 269        """
 270
 271        self.overviewOrdersFile = "overview-orders.md"
 272        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 273
 274        See also: `Overview()` with parameter `details="orders"`.
 275        """
 276
 277        self.overviewAnalyticsFile = "overview-analytics.md"
 278        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 279
 280        See also: `Overview()` with parameter `details="analytics"`.
 281        """
 282
 283        self.overviewBondsCalendarFile = "overview-calendar.md"
 284        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 285
 286        See also: `Overview()` with parameter `details="calendar"`.
 287        """
 288
 289        self.reportFile = "deals.md"
 290        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 291
 292        See also: `Deals()`.
 293        """
 294
 295        self.withdrawalLimitsFile = "limits.md"
 296        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 297
 298        See also: `OverviewLimits()` and `RequestLimits()`.
 299        """
 300
 301        self.userInfoFile = "user-info.md"
 302        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 303
 304        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 305        """
 306
 307        self.userAccountsFile = "accounts.md"
 308        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 309
 310        See also: `OverviewAccounts()`, `RequestAccounts()`.
 311        """
 312
 313        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 314        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 315
 316        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 317
 318        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 319        """
 320
 321        self.iList = None  # init iList for raw instruments data
 322        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 323        
 324        See also: `Listing()`, `DumpInstruments()`.
 325        """
 326
 327        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 328        if useCache:
 329            if os.path.exists(self.iListDumpFile):
 330                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 331                curTime = datetime.now(tzutc())
 332
 333                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 334                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 335
 336                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 337
 338                else:
 339                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 340
 341                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 342                        os.path.abspath(self.iListDumpFile),
 343                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 344                    ))
 345
 346            else:
 347                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 348                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 349
 350        else:
 351            self.iList = self.Listing()  # request new raw instruments data from broker server
 352            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 353
 354        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 355        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 356
 357        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 358        """
 359
 360    @property
 361    def ticker(self) -> str:
 362        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 363
 364        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 365        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 366
 367        See also: `SearchByTicker()`, `SearchInstruments()`.
 368        """
 369        return self._ticker
 370
 371    @ticker.setter
 372    def ticker(self, value):
 373        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 374
 375        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 376        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 377
 378        See also: `SearchByTicker()`, `SearchInstruments()`.
 379        """
 380        self._ticker = str(value).upper()  # Tickers may be upper case only
 381
 382    @property
 383    def figi(self) -> str:
 384        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 385
 386        See also: `SearchByFIGI()`, `SearchInstruments()`.
 387        """
 388        return self._figi
 389
 390    @figi.setter
 391    def figi(self, value):
 392        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 393
 394        See also: `SearchByFIGI()`, `SearchInstruments()`.
 395        """
 396        self._figi = str(value).upper()  # FIGI may be upper case only
 397
 398    def _ParseJSON(self, rawData="{}") -> dict:
 399        """
 400        Parse JSON from response string.
 401
 402        :param rawData: this is a string with JSON-formatted text.
 403        :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`.
 404        """
 405        try:
 406            responseJSON = json.loads(rawData) if rawData else {}
 407
 408            if self.moreDebug:
 409                uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 410
 411            return responseJSON
 412
 413        except Exception as e:
 414            uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e))
 415
 416            return {}
 417
 418    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 419        """
 420        Send GET or POST request to broker server and receive JSON object.
 421
 422        self.header: must be defining with dictionary of headers.
 423        self.body: if define then used as request body. None by default.
 424        self.timeout: global request timeout, 15 seconds by default.
 425        :param url: url with REST request.
 426        :param reqType: send "GET" or "POST" request. "GET" by default.
 427        :param retry: how many times retry after first request if an 5xx server errors occurred.
 428        :param pause: sleep time in seconds between retries.
 429        :return: response JSON (dictionary) from broker.
 430        """
 431        if reqType.upper() not in ("GET", "POST"):
 432            uLogger.error("You can define request type: `GET` or `POST`!")
 433            raise Exception("Incorrect value")
 434
 435        if self.moreDebug:
 436            uLogger.debug("Request parameters:")
 437            uLogger.debug("    - REST API URL: {}".format(url))
 438            uLogger.debug("    - request type: {}".format(reqType))
 439            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 440            uLogger.debug("    - body:\n{}".format(self.body))
 441
 442        # fast hack to avoid all operations with some tickers/FIGI
 443        responseJSON = {}
 444        oK = True
 445        for item in self.exclude:
 446            if item in url:
 447                if self.moreDebug:
 448                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 449
 450                oK = False
 451                break
 452
 453        if oK:
 454            with self.__lock:  # acquire the mutex lock
 455                counter = 0
 456                response = None
 457                errMsg = ""
 458
 459                while not response and counter <= retry:
 460                    if reqType == "GET":
 461                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 462
 463                    if reqType == "POST":
 464                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 465
 466                    if self.moreDebug:
 467                        uLogger.debug("Response:")
 468                        uLogger.debug("    - status code: {}".format(response.status_code))
 469                        uLogger.debug("    - reason: {}".format(response.reason))
 470                        uLogger.debug("    - body length: {}".format(len(response.text)))
 471                        uLogger.debug("    - headers:\n{}".format(response.headers))
 472
 473                    # Server returns some headers:
 474                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 475                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 476                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 477                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 478                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 479                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
 480                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 481                        sleep(rateLimitWait)
 482
 483                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 484                    if 400 <= response.status_code < 500:
 485                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 486                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 487
 488                        if "code" in response.text and "message" in response.text:
 489                            msgDict = self._ParseJSON(rawData=response.text)
 490                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 491
 492                        counter = retry + 1  # do not retry for 4xx errors
 493
 494                    if 500 <= response.status_code < 600:
 495                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 496                        uLogger.debug("    - not oK, {}".format(errMsg))
 497
 498                        if "code" in response.text and "message" in response.text:
 499                            errMsgDict = self._ParseJSON(rawData=response.text)
 500                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 501
 502                        counter += 1
 503
 504                        if counter <= retry:
 505                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 506                            sleep(pause)
 507
 508                responseJSON = self._ParseJSON(rawData=response.text)
 509
 510                if errMsg:
 511                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 512                    uLogger.error("    - not oK, {}".format(errMsg))
 513
 514        return responseJSON
 515
 516    def _IUpdater(self, iType: str) -> tuple:
 517        """
 518        Request instrument by type from server. See available API methods for instruments:
 519        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 520        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 521        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 522        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 523        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 524
 525        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 526        :return: tuple with iType name and list of available instruments of current type for defined user token.
 527        """
 528        result = []
 529
 530        if iType in TKS_INSTRUMENTS:
 531            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 532
 533            # all instruments have the same body in API v2 requests:
 534            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 535            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 536            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 537
 538        return iType, result
 539
 540    def _IWrapper(self, kwargs):
 541        """
 542        Wrapper runs instrument's update method `_IUpdater()`.
 543        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 544        """
 545        return self._IUpdater(**kwargs)
 546
 547    def Listing(self) -> dict:
 548        """
 549        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 550
 551        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 552        """
 553        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 554        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 555
 556        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 557        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 558        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 559
 560        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 561        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 562        poolUpdater.close()  # close the thread pool
 563        poolUpdater.join()  # wait a moment until all data returns from threads
 564
 565        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 566        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 567        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 568
 569        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 570        for iType in iList.keys():
 571            for ticker in iList[iType]:
 572                iList[iType][ticker]["type"] = iType
 573
 574                if "minPriceIncrement" in iList[iType][ticker].keys():
 575                    iList[iType][ticker]["step"] = NanoToFloat(
 576                        iList[iType][ticker]["minPriceIncrement"]["units"],
 577                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 578                    )
 579
 580                else:
 581                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 582
 583        return iList
 584
 585    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 586        """
 587        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 588
 589        See also: `DumpInstruments()`, `Listing()`.
 590
 591        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 592                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 593        """
 594        if self.iListDumpFile is None or not self.iListDumpFile:
 595            uLogger.error("Output name of dump file must be defined!")
 596            raise Exception("Filename required")
 597
 598        if not self.iList or forceUpdate:
 599            self.iList = self.Listing()
 600
 601        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 602
 603        # Save as XLSX with separated sheets for every type of instruments:
 604        with pd.ExcelWriter(
 605                path=xlsxDumpFile,
 606                date_format=TKS_DATE_FORMAT,
 607                datetime_format=TKS_DATE_TIME_FORMAT,
 608                mode="w",
 609        ) as writer:
 610            for iType in TKS_INSTRUMENTS:
 611                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 612                df = df[sorted(df)]  # sorted by column names
 613                df = df.applymap(
 614                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 615                    na_action="ignore",
 616                )  # converting numbers from nano-type to float in every cell
 617                df.to_excel(
 618                    writer,
 619                    sheet_name=iType,
 620                    encoding="UTF-8",
 621                    freeze_panes=(1, 1),
 622                )  # saving as XLSX-file with freeze first row and column as headers
 623
 624        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 625
 626    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 627        """
 628        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 629        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 630
 631        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 632
 633        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 634                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 635        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 636        """
 637        if self.iListDumpFile is None or not self.iListDumpFile:
 638            uLogger.error("Output name of dump file must be defined!")
 639            raise Exception("Filename required")
 640
 641        if not self.iList or forceUpdate:
 642            self.iList = self.Listing()
 643
 644        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 645        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 646            fH.write(jsonDump)
 647
 648        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 649
 650        return jsonDump
 651
 652    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 653        """
 654        Show information about one instrument defined by json data and prints it in Markdown format.
 655
 656        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 657
 658        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 659        :param show: if `True` then also printing information about instrument and its current price.
 660        :return: multilines text in Markdown format with information about one instrument.
 661        """
 662        splitLine = "|                                                             |                                                        |\n"
 663        infoText = ""
 664
 665        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 666            info = [
 667                "# Main information\n\n",
 668                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 669                "| Parameters                                                  | Values                                                 |\n",
 670                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 671                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 672                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 673            ]
 674
 675            if "sector" in iJSON.keys() and iJSON["sector"]:
 676                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 677
 678            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 679                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 680
 681            info.extend([
 682                splitLine,
 683                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 684                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 685            ])
 686
 687            if "isin" in iJSON.keys() and iJSON["isin"]:
 688                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 689
 690            if "classCode" in iJSON.keys():
 691                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 692
 693            info.extend([
 694                splitLine,
 695                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 696                splitLine,
 697                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 698                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 699                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 700            ])
 701
 702            if iJSON["figi"]:
 703                self._figi = iJSON["figi"]
 704                iJSON = iJSON | self.RequestTradingStatus()
 705
 706                info.extend([
 707                    splitLine,
 708                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 709                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 710                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 711                ])
 712
 713            info.append(splitLine)
 714
 715            if "type" in iJSON.keys() and iJSON["type"]:
 716                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 717
 718                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 719                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 720
 721            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 722                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 723
 724            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 725                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 726
 727            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 728                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 729
 730            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 731                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 732
 733            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 734                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 735
 736            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 737                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 738
 739            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 740                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 741
 742            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 743                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 744
 745            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 746                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 747
 748            if "currency" in iJSON.keys():
 749                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 750
 751            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 752                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 753
 754            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 755                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 756
 757            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 758                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 759
 760            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 761                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 762
 763            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 764                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 765
 766            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 767                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 768
 769            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 770                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 771
 772            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 773                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 774
 775            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 776                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 777
 778            iExt = None
 779            if iJSON["type"] == "Bonds":
 780                info.extend([
 781                    splitLine,
 782                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 783                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 784                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 785                        iJSON["nominal"]["currency"],
 786                    )),
 787                ])
 788
 789                if "floatingCouponFlag" in iJSON.keys():
 790                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 791
 792                if "amortizationFlag" in iJSON.keys():
 793                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 794
 795                info.append(splitLine)
 796
 797                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 798                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 799
 800                if iJSON["figi"]:
 801                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 802
 803                    info.extend([
 804                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 805                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 806                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 807                    ])
 808
 809                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 810                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 811                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 812                        iJSON["aciValue"]["currency"]
 813                    )))
 814
 815            if "currentPrice" in iJSON.keys():
 816                info.append(splitLine)
 817
 818                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 819                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 820
 821                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 822                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 823                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 824                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 825                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 826
 827                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 828                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 829
 830                info.extend([
 831                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 832                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 833                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 834                    )),
 835                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 836                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 837                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 838                    )),
 839                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 840                        "{:.2f}%{}".format(
 841                            iJSON["currentPrice"]["changes"],
 842                            " ({}{:.2f} {})".format(
 843                                "+" if bondChangesDelta > 0 else "",
 844                                bondChangesDelta,
 845                                aciCurrency
 846                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 847                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 848                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 849                                currency
 850                            ),
 851                        )
 852                    ),
 853                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 854                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 855                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 856                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 857                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 858                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 859                    )),
 860                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 861                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 862                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 863                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 864                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 865                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 866                    )),
 867                ])
 868
 869            if "lot" in iJSON.keys():
 870                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 871
 872            if "step" in iJSON.keys() and iJSON["step"] != 0:
 873                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 874
 875            # Add bond payment calendar:
 876            if iJSON["type"] == "Bonds":
 877                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 878                info.extend(["\n#", strCalendar])
 879
 880            infoText += "".join(info)
 881
 882            if show:
 883                uLogger.info("{}".format(infoText))
 884
 885            else:
 886                uLogger.debug("{}".format(infoText))
 887
 888            if self.infoFile is not None:
 889                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 890                    fH.write(infoText)
 891
 892                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 893
 894                if self.useHTMLReports:
 895                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 896                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 897                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 898
 899                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 900
 901        return infoText
 902
 903    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 904        """
 905        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 906
 907        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 908        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 909        :return: JSON formatted data with information about instrument.
 910        """
 911        tickerJSON = {}
 912        if self.moreDebug:
 913            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 914
 915        if not self._ticker:
 916            uLogger.warning("self._ticker variable is not be empty!")
 917
 918        else:
 919            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 920                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 921                raise Exception("Instrument not allowed")
 922
 923            if not self.iList:
 924                self.iList = self.Listing()
 925
 926            if self._ticker in self.iList["Shares"].keys():
 927                tickerJSON = self.iList["Shares"][self._ticker]
 928                if self.moreDebug:
 929                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 930
 931            elif self._ticker in self.iList["Currencies"].keys():
 932                tickerJSON = self.iList["Currencies"][self._ticker]
 933                if self.moreDebug:
 934                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 935
 936            elif self._ticker in self.iList["Bonds"].keys():
 937                tickerJSON = self.iList["Bonds"][self._ticker]
 938                if self.moreDebug:
 939                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 940
 941            elif self._ticker in self.iList["Etfs"].keys():
 942                tickerJSON = self.iList["Etfs"][self._ticker]
 943                if self.moreDebug:
 944                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 945
 946            elif self._ticker in self.iList["Futures"].keys():
 947                tickerJSON = self.iList["Futures"][self._ticker]
 948                if self.moreDebug:
 949                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 950
 951        if tickerJSON:
 952            self._figi = tickerJSON["figi"]
 953
 954            if requestPrice:
 955                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 956
 957                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 958                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 959
 960                else:
 961                    tickerJSON["currentPrice"]["changes"] = 0
 962
 963            if show:
 964                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 965
 966        else:
 967            if show:
 968                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 969
 970        return tickerJSON
 971
 972    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 973        """
 974        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 975
 976        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 977        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 978        :return: JSON formatted data with information about instrument.
 979        """
 980        figiJSON = {}
 981        if self.moreDebug:
 982            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 983
 984        if not self._figi:
 985            uLogger.warning("self._figi variable is not be empty!")
 986
 987        else:
 988            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 989                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 990                raise Exception("Instrument not allowed")
 991
 992            if not self.iList:
 993                self.iList = self.Listing()
 994
 995            for item in self.iList["Shares"].keys():
 996                if self._figi == self.iList["Shares"][item]["figi"]:
 997                    figiJSON = self.iList["Shares"][item]
 998
 999                    if self.moreDebug:
1000                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
1001
1002                    break
1003
1004            if not figiJSON:
1005                for item in self.iList["Currencies"].keys():
1006                    if self._figi == self.iList["Currencies"][item]["figi"]:
1007                        figiJSON = self.iList["Currencies"][item]
1008
1009                        if self.moreDebug:
1010                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1011
1012                        break
1013
1014            if not figiJSON:
1015                for item in self.iList["Bonds"].keys():
1016                    if self._figi == self.iList["Bonds"][item]["figi"]:
1017                        figiJSON = self.iList["Bonds"][item]
1018
1019                        if self.moreDebug:
1020                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1021
1022                        break
1023
1024            if not figiJSON:
1025                for item in self.iList["Etfs"].keys():
1026                    if self._figi == self.iList["Etfs"][item]["figi"]:
1027                        figiJSON = self.iList["Etfs"][item]
1028
1029                        if self.moreDebug:
1030                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1031
1032                        break
1033
1034            if not figiJSON:
1035                for item in self.iList["Futures"].keys():
1036                    if self._figi == self.iList["Futures"][item]["figi"]:
1037                        figiJSON = self.iList["Futures"][item]
1038
1039                        if self.moreDebug:
1040                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1041
1042                        break
1043
1044        if figiJSON:
1045            self._figi = figiJSON["figi"]
1046            self._ticker = figiJSON["ticker"]
1047
1048            if requestPrice:
1049                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1050
1051                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1052                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1053
1054                else:
1055                    figiJSON["currentPrice"]["changes"] = 0
1056
1057            if show:
1058                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1059
1060        else:
1061            if show:
1062                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1063
1064        return figiJSON
1065
1066    def GetCurrentPrices(self, show: bool = True) -> dict:
1067        """
1068        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1069        `{"buy": [{"price": 1243.8, "quantity": 193},
1070                  {"price": 1244.0, "quantity": 168},
1071                  {"price": 1244.8, "quantity": 5},
1072                  {"price": 1245.0, "quantity": 61},
1073                  {"price": 1245.4, "quantity": 60}],
1074          "sell": [{"price": 1243.6, "quantity": 8},
1075                   {"price": 1242.6, "quantity": 10},
1076                   {"price": 1242.4, "quantity": 18},
1077                   {"price": 1242.2, "quantity": 50},
1078                   {"price": 1242.0, "quantity": 113}],
1079          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1080        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1081        - sell: list of dicts with Buyers prices,
1082            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1083            - quantity: volume value by current price in lots,
1084        - limitUp: current trade session limit price, maximum,
1085        - limitDown: current trade session limit price, minimum,
1086        - lastPrice: last deal price of the instrument,
1087        - closePrice: previous trade session close price of the instrument.
1088
1089        See also: `SearchByTicker()` and `SearchByFIGI()`.
1090        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1091        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1092
1093        :param show: if `True` then print DOM to log and console.
1094        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1095                 If an error occurred then returns an empty record:
1096                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1097        """
1098        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1099
1100        if self.depth < 1:
1101            uLogger.error("Depth of Market (DOM) must be >=1!")
1102            raise Exception("Incorrect value")
1103
1104        if not (self._ticker or self._figi):
1105            uLogger.error("self._ticker or self._figi variables must be defined!")
1106            raise Exception("Ticker or FIGI required")
1107
1108        if self._ticker and not self._figi:
1109            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1110            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1111
1112        if not self._ticker and self._figi:
1113            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1114            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1115
1116        if not self._figi:
1117            uLogger.error("FIGI is not defined!")
1118            raise Exception("Ticker or FIGI required")
1119
1120        else:
1121            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1122
1123            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1124            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1125            self.body = str({"figi": self._figi, "depth": self.depth})
1126            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1127
1128            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1129                # list of dicts with sellers orders:
1130                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1131
1132                # list of dicts with buyers orders:
1133                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1134
1135                # max price of instrument at this time:
1136                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1137
1138                # min price of instrument at this time:
1139                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1140
1141                # last price of deal with instrument:
1142                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1143
1144                # last close price of instrument:
1145                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1146
1147            else:
1148                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1149                uLogger.debug("Server response: {}".format(pricesResponse))
1150
1151            if show:
1152                if prices["buy"] or prices["sell"]:
1153                    info = [
1154                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1155                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1156                            self._ticker,
1157                            self._figi,
1158                            self.depth,
1159                        ),
1160                        "-" * 60, "\n",
1161                        "             Orders of Buyers | Orders of Sellers\n",
1162                        "-" * 60, "\n",
1163                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1164                        "-" * 60, "\n",
1165                    ]
1166
1167                    if not prices["buy"]:
1168                        info.append("                              | No orders!\n")
1169                        sumBuy = 0
1170
1171                    else:
1172                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1173                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1174                        for item in maxMinSorted:
1175                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1176
1177                    if not prices["sell"]:
1178                        info.append("No orders!                    |\n")
1179                        sumSell = 0
1180
1181                    else:
1182                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1183                        for item in prices["sell"]:
1184                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1185
1186                    info.extend([
1187                        "-" * 60, "\n",
1188                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1189                        "-" * 60, "\n",
1190                    ])
1191
1192                    infoText = "".join(info)
1193
1194                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1195
1196                else:
1197                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1198
1199        return prices
1200
1201    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1202        """
1203        This method get and show information about all available broker instruments for current user account.
1204        If `instrumentsFile` string is not empty then also save information to this file.
1205
1206        :param show: if `True` then print results to console, if `False` — print only to file.
1207        :return: multi-lines string with all available broker instruments
1208        """
1209        if not self.iList:
1210            self.iList = self.Listing()
1211
1212        info = [
1213            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1214            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1215        ]
1216
1217        # add instruments count by type:
1218        for iType in self.iList.keys():
1219            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1220
1221        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1222        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1223
1224        # generating info tables with all instruments by type:
1225        for iType in self.iList.keys():
1226            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1227
1228            for instrument in self.iList[iType].keys():
1229                iName = self.iList[iType][instrument]["name"]  # instrument's name
1230                if len(iName) > 57:
1231                    iName = "{}...".format(iName[:54])  # right trim for a long string
1232
1233                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1234                    self.iList[iType][instrument]["ticker"],
1235                    iName,
1236                    self.iList[iType][instrument]["figi"],
1237                    self.iList[iType][instrument]["currency"],
1238                    self.iList[iType][instrument]["lot"],
1239                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1240                ))
1241
1242        infoText = "".join(info)
1243
1244        if show:
1245            uLogger.info(infoText)
1246
1247        if self.instrumentsFile:
1248            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1249                fH.write(infoText)
1250
1251            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1252
1253            if self.useHTMLReports:
1254                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1255                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1256                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1257
1258                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1259
1260        return infoText
1261
1262    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1263        """
1264        This method search and show information about instruments by part of its ticker, FIGI or name.
1265        If `searchResultsFile` string is not empty then also save information to this file.
1266
1267        :param pattern: string with part of ticker, FIGI or instrument's name.
1268        :param show: if `True` then print results to console, if `False` — return list of result only.
1269        :return: list of dictionaries with all found instruments.
1270        """
1271        if not self.iList:
1272            self.iList = self.Listing()
1273
1274        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1275        compiledPattern = re.compile(pattern, re.IGNORECASE)
1276
1277        for iType in self.iList:
1278            for instrument in self.iList[iType].values():
1279                searchResult = compiledPattern.search(" ".join(
1280                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1281                ))
1282
1283                if searchResult:
1284                    searchResults[iType][instrument["ticker"]] = instrument
1285
1286        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1287        info = [
1288            "# Search results\n\n",
1289            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1290            "* **Search pattern:** [{}]\n".format(pattern),
1291            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1292            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1293        ]
1294        infoShort = info[:]
1295
1296        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1297        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1298        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1299
1300        if resultsLen == 0:
1301            info.append("\nNo results\n")
1302            infoShort.append("\nNo results\n")
1303            uLogger.warning("No results. Try changing your search pattern.")
1304
1305        else:
1306            for iType in searchResults:
1307                iTypeValuesCount = len(searchResults[iType].values())
1308                if iTypeValuesCount > 0:
1309                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1310                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1311
1312                    for instrument in searchResults[iType].values():
1313                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1314                            instrument["type"],
1315                            instrument["ticker"],
1316                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1317                            instrument["figi"],
1318                        ))
1319
1320                    if iTypeValuesCount <= 5:
1321                        infoShort.extend(info[-iTypeValuesCount:])
1322
1323                    else:
1324                        infoShort.extend(info[-5:])
1325                        infoShort.append(skippedLine)
1326
1327        infoText = "".join(info)
1328        infoTextShort = "".join(infoShort)
1329
1330        if show:
1331            uLogger.info(infoTextShort)
1332            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1333
1334        if self.searchResultsFile:
1335            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1336                fH.write(infoText)
1337
1338            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1339
1340            if self.useHTMLReports:
1341                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1342                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1343                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1344
1345                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1346
1347        return searchResults
1348
1349    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1350        """
1351        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1352
1353        :param instruments: list of strings with tickers or FIGIs.
1354        :return: list with unique instrument FIGIs only.
1355        """
1356        requestedInstruments = []
1357        for iName in instruments:
1358            if iName not in self.aliases.keys():
1359                if iName not in requestedInstruments:
1360                    requestedInstruments.append(iName)
1361
1362            else:
1363                if iName not in requestedInstruments:
1364                    if self.aliases[iName] not in requestedInstruments:
1365                        requestedInstruments.append(self.aliases[iName])
1366
1367        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1368
1369        onlyUniqueFIGIs = []
1370        for iName in requestedInstruments:
1371            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1372                continue
1373
1374            self._ticker = iName
1375            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1376
1377            if not iData:
1378                self._ticker = ""
1379                self._figi = iName
1380
1381                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1382
1383                if not iData:
1384                    self._figi = ""
1385                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1386
1387            if iData and iData["figi"] not in onlyUniqueFIGIs:
1388                onlyUniqueFIGIs.append(iData["figi"])
1389
1390        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1391
1392        return onlyUniqueFIGIs
1393
1394    def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1395        """
1396        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1397
1398        See limits: https://tinkoff.github.io/investAPI/limits/
1399
1400        If `pricesFile` string is not empty then also save information to this file.
1401
1402        :param instruments: list of strings with tickers or FIGIs.
1403        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1404        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1405                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1406        """
1407        if instruments is None or not instruments:
1408            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1409            raise Exception("Ticker or FIGI required")
1410
1411        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1412
1413        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1414
1415        iList = []  # trying to get info and current prices about all unique instruments:
1416        for self._figi in onlyUniqueFIGIs:
1417            iData = self.SearchByFIGI(requestPrice=True)
1418            iList.append(iData)
1419
1420        self.ShowListOfPrices(iList, show)
1421
1422        return iList
1423
1424    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1425        """
1426        Show table contains current prices of given instruments.
1427
1428        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1429                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1430        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1431        :return: multilines text in Markdown format as a table contains current prices.
1432        """
1433        infoText = ""
1434
1435        if show or self.pricesFile:
1436            info = [
1437                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1438                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1439                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1440            ]
1441
1442            for item in iList:
1443                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1444                    item["ticker"],
1445                    item["figi"],
1446                    item["type"],
1447                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1448                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1449                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1450                    "{} / {}".format(
1451                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1452                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1453                    ),
1454                    "{} / {}".format(
1455                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1456                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1457                    ),
1458                    item["currency"],
1459                ))
1460
1461            infoText = "".join(info)
1462
1463            if show:
1464                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1465
1466            if self.pricesFile:
1467                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1468                    fH.write(infoText)
1469
1470                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1471
1472                if self.useHTMLReports:
1473                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1474                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1475                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1476
1477                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1478
1479        return infoText
1480
1481    def RequestTradingStatus(self) -> dict:
1482        """
1483        Requesting trading status for the instrument defined by `figi` variable.
1484
1485        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1486
1487        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1488
1489        :return: dictionary with trading status attributes. Response example:
1490                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1491                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1492        """
1493        if self._figi is None or not self._figi:
1494            uLogger.error("Variable `figi` must be defined for using this method!")
1495            raise Exception("FIGI required")
1496
1497        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1498
1499        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1500        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1501        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1502
1503        if self.moreDebug:
1504            uLogger.debug("Records about current trading status successfully received")
1505
1506        return tradingStatus
1507
1508    def RequestPortfolio(self) -> dict:
1509        """
1510        Requesting actual user's portfolio for current `accountId`.
1511
1512        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1513
1514        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1515
1516        :return: dictionary with user's portfolio.
1517        """
1518        if self.accountId is None or not self.accountId:
1519            uLogger.error("Variable `accountId` must be defined for using this method!")
1520            raise Exception("Account ID required")
1521
1522        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1523
1524        self.body = str({"accountId": self.accountId})
1525        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1526        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1527
1528        if self.moreDebug:
1529            uLogger.debug("Records about user's portfolio successfully received")
1530
1531        return rawPortfolio
1532
1533    def RequestPositions(self) -> dict:
1534        """
1535        Requesting open positions by currencies and instruments for current `accountId`.
1536
1537        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1538
1539        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1540
1541        :return: dictionary with open positions by instruments.
1542        """
1543        if self.accountId is None or not self.accountId:
1544            uLogger.error("Variable `accountId` must be defined for using this method!")
1545            raise Exception("Account ID required")
1546
1547        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1548
1549        self.body = str({"accountId": self.accountId})
1550        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1551        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1552
1553        if self.moreDebug:
1554            uLogger.debug("Records about current open positions successfully received")
1555
1556        return rawPositions
1557
1558    def RequestPendingOrders(self) -> list:
1559        """
1560        Requesting current actual pending limit orders for current `accountId`.
1561
1562        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1563
1564        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1565
1566        :return: list of dictionaries with pending limit orders.
1567        """
1568        if self.accountId is None or not self.accountId:
1569            uLogger.error("Variable `accountId` must be defined for using this method!")
1570            raise Exception("Account ID required")
1571
1572        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1573
1574        self.body = str({"accountId": self.accountId})
1575        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1576        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1577
1578        if "orders" in rawResponse.keys():
1579            rawOrders = rawResponse["orders"]
1580            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1581
1582        else:
1583            rawOrders = []
1584            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1585
1586        return rawOrders
1587
1588    def RequestStopOrders(self) -> list:
1589        """
1590        Requesting current actual stop orders for current `accountId`.
1591
1592        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1593
1594        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1595
1596        :return: list of dictionaries with stop orders.
1597        """
1598        if self.accountId is None or not self.accountId:
1599            uLogger.error("Variable `accountId` must be defined for using this method!")
1600            raise Exception("Account ID required")
1601
1602        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1603
1604        self.body = str({"accountId": self.accountId})
1605        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1606        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1607
1608        if "stopOrders" in rawResponse.keys():
1609            rawStopOrders = rawResponse["stopOrders"]
1610            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1611
1612        else:
1613            rawStopOrders = []
1614            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1615
1616        return rawStopOrders
1617
1618    def Overview(self, show: bool = False, details: str = "full") -> dict:
1619        """
1620        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1621        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1622        and `overviewBondsCalendarFile` are defined then also save information to file.
1623
1624        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1625        many requests about the state of the portfolio, and then, based on the received data, a large number
1626        of calculation and statistics are collected.
1627
1628        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1629        :param details: how detailed should the information be?
1630        - `full` — shows full available information about portfolio status (by default),
1631        - `positions` — shows only open positions,
1632        - `orders` — shows only sections of open limits and stop orders.
1633        - `digest` — show a short digest of the portfolio status,
1634        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1635        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1636        :return: dictionary with client's raw portfolio and some statistics.
1637        """
1638        if self.accountId is None or not self.accountId:
1639            uLogger.error("Variable `accountId` must be defined for using this method!")
1640            raise Exception("Account ID required")
1641
1642        view = {
1643            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1644                "headers": {},  # list of dictionaries, response headers without "positions" section
1645                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1646                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1647                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1648                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1649                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1650                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1651                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1652                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1653                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1654            },
1655            "stat": {  # --- some statistics calculated using "raw" sections:
1656                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1657                "availableRUB": 0.,  # available rubles (without other currencies)
1658                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1659                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1660                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1661                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1662                "sharesCostRUB": 0.,  # costs of all shares in RUB
1663                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1664                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1665                "futuresCostRUB": 0.,  # costs of all futures in RUB
1666                "Currencies": [],  # list of dictionaries of all currencies statistics
1667                "Shares": [],  # list of dictionaries of all shares statistics
1668                "Bonds": [],  # list of dictionaries of all bonds statistics
1669                "Etfs": [],  # list of dictionaries of all etfs statistics
1670                "Futures": [],  # list of dictionaries of all futures statistics
1671                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1672                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1673                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1674                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1675                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1676            },
1677            "analytics": {  # --- some analytics of portfolio:
1678                "distrByAssets": {},  # portfolio distribution by assets
1679                "distrByCompanies": {},  # portfolio distribution by companies
1680                "distrBySectors": {},  # portfolio distribution by sectors
1681                "distrByCurrencies": {},  # portfolio distribution by currencies
1682                "distrByCountries": {},  # portfolio distribution by countries
1683                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1684            }
1685        }
1686
1687        details = details.lower()
1688        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1689        if details not in availableDetails:
1690            details = "full"
1691            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1692
1693        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1694
1695        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1696        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1697        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1698        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1699
1700        # save response headers without "positions" section:
1701        for key in portfolioResponse.keys():
1702            if key != "positions":
1703                view["raw"]["headers"][key] = portfolioResponse[key]
1704
1705            else:
1706                continue
1707
1708        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1709        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1710        for item in portfolioResponse["positions"]:
1711            if item["instrumentType"] == "currency":
1712                self._figi = item["figi"]
1713                if not self._figi and item["ticker"]:
1714                    self._ticker = item["ticker"]
1715                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1716
1717                curr = self.SearchByFIGI(requestPrice=False)
1718
1719                # current price of currency in RUB:
1720                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1721                    "name": curr["name"],
1722                    "currentPrice": NanoToFloat(
1723                        item["currentPrice"]["units"],
1724                        item["currentPrice"]["nano"]
1725                    ),
1726                }
1727
1728                view["raw"]["Currencies"].append(item)
1729
1730            elif item["instrumentType"] == "share":
1731                view["raw"]["Shares"].append(item)
1732
1733            elif item["instrumentType"] == "bond":
1734                view["raw"]["Bonds"].append(item)
1735
1736            elif item["instrumentType"] == "etf":
1737                view["raw"]["Etfs"].append(item)
1738
1739            elif item["instrumentType"] == "futures":
1740                view["raw"]["Futures"].append(item)
1741
1742            else:
1743                continue
1744
1745        # how many volume of currencies (by ISO currency name) are blocked:
1746        for item in view["raw"]["positions"]["blocked"]:
1747            blocked = NanoToFloat(item["units"], item["nano"])
1748            if blocked > 0:
1749                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1750
1751        # how many volume of instruments (by FIGI) are blocked:
1752        for item in view["raw"]["positions"]["securities"]:
1753            blocked = int(item["blocked"])
1754            if blocked > 0:
1755                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1756
1757        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1758
1759        if "rub" in allBlocked.keys():
1760            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1761
1762        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1763        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1764        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1765        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1766        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1767        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1768        view["stat"]["portfolioCostRUB"] = sum([
1769            view["stat"]["allCurrenciesCostRUB"],
1770            view["stat"]["sharesCostRUB"],
1771            view["stat"]["bondsCostRUB"],
1772            view["stat"]["etfsCostRUB"],
1773            view["stat"]["futuresCostRUB"],
1774        ])
1775
1776        # --- calculating some portfolio statistics:
1777        byComp = {}  # distribution by companies
1778        bySect = {}  # distribution by sectors
1779        byCurr = {}  # distribution by currencies (include RUB)
1780        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1781        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1782
1783        for item in portfolioResponse["positions"]:
1784            self._figi = item["figi"]
1785            if not self._figi and item["ticker"]:
1786                self._ticker = item["ticker"]
1787                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1788
1789            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1790
1791            if instrument:
1792                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1793                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1794
1795                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1796                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1797
1798                else:
1799                    blocked = 0
1800
1801                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1802                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1803                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1804                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1805                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1806                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1807                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1808                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1809                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1810                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1811                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1812                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1813
1814                statData = {
1815                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1816                    "ticker": instrument["ticker"],  # ticker by FIGI
1817                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1818                    "volume": volume,  # available volume of instrument
1819                    "lots": lots,  # volume in lots of instrument
1820                    "direction": direction,  # direction of an instrument's position: short or long
1821                    "blocked": blocked,  # blocked volume of currency or instrument
1822                    "currentPrice": curPrice,  # current instrument's price in basic asset
1823                    "average": average,  # current average position price
1824                    "cost": cost,  # current cost of all volume of instrument in basic asset
1825                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1826                    "costRUB": costRUB,  # cost of instrument in ruble
1827                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1828                    "profit": profit,  # expected profit at current moment
1829                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1830                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1831                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1832                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1833                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1834                    "step": instrument["step"],  # minimum price increment
1835                }
1836
1837                # adding distribution by unique countries:
1838                if statData["country"] not in byCountry.keys():
1839                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1840
1841                else:
1842                    byCountry[statData["country"]]["cost"] += costRUB
1843                    byCountry[statData["country"]]["percent"] += percentCostRUB
1844
1845                if item["instrumentType"] != "currency":
1846                    # adding distribution by unique companies:
1847                    if statData["name"]:
1848                        if statData["name"] not in byComp.keys():
1849                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1850
1851                        else:
1852                            byComp[statData["name"]]["cost"] += costRUB
1853                            byComp[statData["name"]]["percent"] += percentCostRUB
1854
1855                    # adding distribution by unique sectors:
1856                    if statData["sector"] not in bySect.keys():
1857                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1858
1859                    else:
1860                        bySect[statData["sector"]]["cost"] += costRUB
1861                        bySect[statData["sector"]]["percent"] += percentCostRUB
1862
1863                # adding distribution by unique currencies:
1864                if currency not in byCurr.keys():
1865                    byCurr[currency] = {
1866                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1867                        "cost": costRUB,
1868                        "percent": percentCostRUB
1869                    }
1870
1871                else:
1872                    byCurr[currency]["cost"] += costRUB
1873                    byCurr[currency]["percent"] += percentCostRUB
1874
1875                # saving statistics for every instrument:
1876                if item["instrumentType"] == "currency":
1877                    view["stat"]["Currencies"].append(statData)
1878
1879                    # update dict with free funds for trading (total - blocked) by currencies
1880                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1881                    view["stat"]["funds"][currency] = {
1882                        "total": volume,
1883                        "totalCostRUB": costRUB,  # total volume cost in rubles
1884                        "free": volume - blocked,
1885                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1886                    }
1887
1888                elif item["instrumentType"] == "share":
1889                    view["stat"]["Shares"].append(statData)
1890
1891                elif item["instrumentType"] == "bond":
1892                    view["stat"]["Bonds"].append(statData)
1893
1894                elif item["instrumentType"] == "etf":
1895                    view["stat"]["Etfs"].append(statData)
1896
1897                elif item["instrumentType"] == "Futures":
1898                    view["stat"]["Futures"].append(statData)
1899
1900                else:
1901                    continue
1902
1903        # total changes in Russian Ruble:
1904        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1905        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1906        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1907        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1908        view["stat"]["funds"]["rub"] = {
1909            "total": view["stat"]["availableRUB"],
1910            "totalCostRUB": view["stat"]["availableRUB"],
1911            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1912            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1913        }
1914
1915        # --- pending limit orders sector data:
1916        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1917        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1918
1919        for item in view["raw"]["orders"]:
1920            self._figi = item["figi"]
1921
1922            if item["figi"] not in uniquePendingOrdersFIGIs:
1923                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1924
1925                uniquePendingOrdersFIGIs.append(item["figi"])
1926                uniquePendingOrders[item["figi"]] = instrument
1927
1928            else:
1929                instrument = uniquePendingOrders[item["figi"]]
1930
1931            if instrument:
1932                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1933                orderType = TKS_ORDER_TYPES[item["orderType"]]
1934                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1935                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1936
1937                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1938                if item["direction"] == "ORDER_DIRECTION_BUY":
1939                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1940
1941                else:
1942                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1943
1944                # requested price for order execution:
1945                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1946
1947                # necessary changes in percent to reach target from current price:
1948                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1949
1950                view["stat"]["orders"].append({
1951                    "orderID": item["orderId"],  # orderId number parameter of current order
1952                    "figi": item["figi"],  # FIGI identification
1953                    "ticker": instrument["ticker"],  # ticker name by FIGI
1954                    "lotsRequested": item["lotsRequested"],  # requested lots value
1955                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1956                    "currentPrice": lastPrice,  # current instrument's price for defined action
1957                    "targetPrice": target,  # requested price for order execution in base currency
1958                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1959                    "percentChanges": changes,  # changes in percent to target from current price
1960                    "currency": item["currency"],  # instrument's currency name
1961                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1962                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1963                    "status": orderState,  # order status from TKS_ORDER_STATES
1964                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1965                })
1966
1967        # --- stop orders sector data:
1968        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1969        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1970
1971        for item in view["raw"]["stopOrders"]:
1972            self._figi = item["figi"]
1973
1974            if item["figi"] not in uniqueStopOrdersFIGIs:
1975                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1976
1977                uniqueStopOrdersFIGIs.append(item["figi"])
1978                uniqueStopOrders[item["figi"]] = instrument
1979
1980            else:
1981                instrument = uniqueStopOrders[item["figi"]]
1982
1983            if instrument:
1984                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1985                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1986                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1987
1988                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1989                if "expirationTime" in item.keys():
1990                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1991                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1992
1993                else:
1994                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1995                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1996
1997                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1998                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
1999                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
2000
2001                else:
2002                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2003
2004                # requested price when stop-order executed:
2005                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2006
2007                # price for limit-order, set up when stop-order executed:
2008                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2009
2010                # necessary changes in percent to reach target from current price:
2011                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2012
2013                view["stat"]["stopOrders"].append({
2014                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2015                    "figi": item["figi"],  # FIGI identification
2016                    "ticker": instrument["ticker"],  # ticker name by FIGI
2017                    "lotsRequested": item["lotsRequested"],  # requested lots value
2018                    "currentPrice": lastPrice,  # current instrument's price for defined action
2019                    "targetPrice": target,  # requested price for stop-order execution in base currency
2020                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2021                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2022                    "percentChanges": changes,  # changes in percent to target from current price
2023                    "currency": item["currency"],  # instrument's currency name
2024                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2025                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2026                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2027                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2028                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2029                })
2030
2031        # --- calculating data for analytics section:
2032        # portfolio distribution by assets:
2033        view["analytics"]["distrByAssets"] = {
2034            "Ruble": {
2035                "uniques": 1,
2036                "cost": view["stat"]["availableRUB"],
2037                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2038            },
2039            "Currencies": {
2040                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2041                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2042                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2043            },
2044            "Shares": {
2045                "uniques": len(view["stat"]["Shares"]),
2046                "cost": view["stat"]["sharesCostRUB"],
2047                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2048            },
2049            "Bonds": {
2050                "uniques": len(view["stat"]["Bonds"]),
2051                "cost": view["stat"]["bondsCostRUB"],
2052                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2053            },
2054            "Etfs": {
2055                "uniques": len(view["stat"]["Etfs"]),
2056                "cost": view["stat"]["etfsCostRUB"],
2057                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2058            },
2059            "Futures": {
2060                "uniques": len(view["stat"]["Futures"]),
2061                "cost": view["stat"]["futuresCostRUB"],
2062                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2063            },
2064        }
2065
2066        # portfolio distribution by companies:
2067        view["analytics"]["distrByCompanies"]["All money cash"] = {
2068            "ticker": "",
2069            "cost": view["stat"]["allCurrenciesCostRUB"],
2070            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2071        }
2072        view["analytics"]["distrByCompanies"].update(byComp)
2073
2074        # portfolio distribution by sectors:
2075        view["analytics"]["distrBySectors"]["All money cash"] = {
2076            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2077            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2078        }
2079        view["analytics"]["distrBySectors"].update(bySect)
2080
2081        # portfolio distribution by currencies:
2082        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2083            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2084
2085            if self.moreDebug:
2086                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2087
2088        view["analytics"]["distrByCurrencies"].update(byCurr)
2089        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2090        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2091
2092        # portfolio distribution by countries:
2093        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2094            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2095
2096            if self.moreDebug:
2097                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2098
2099        view["analytics"]["distrByCountries"].update(byCountry)
2100        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2101        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2102
2103        # --- Prepare text statistics overview in human-readable:
2104        if show:
2105            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2106
2107            # Whatever the value `details`, header not changes:
2108            info = [
2109                "# Client's portfolio\n\n",
2110                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2111                "* **Account ID:** [{}]\n".format(self.accountId),
2112            ]
2113
2114            if details in ["full", "positions", "digest"]:
2115                info.extend([
2116                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2117                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2118                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2119                        view["stat"]["totalChangesRUB"],
2120                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2121                        view["stat"]["totalChangesPercentRUB"],
2122                    ),
2123                ])
2124
2125            if details in ["full", "positions"]:
2126                info.extend([
2127                    "## Open positions\n\n",
2128                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2129                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2130                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2131                        "{:.2f} ({:.2f}) rub".format(
2132                            view["stat"]["availableRUB"],
2133                            view["stat"]["blockedRUB"],
2134                        )
2135                    )
2136                ])
2137
2138                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2139                    return [
2140                        "|                             |                                 |          |              |              |                     |                              |\n",
2141                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2142                            noTradeStr if noTradeStr else typeStr,
2143                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2144                        ),
2145                    ]
2146
2147                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2148                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2149                        "{} [{}]".format(data["ticker"], data["figi"]),
2150                        "{:.2f} ({:.2f}) {}".format(
2151                            data["volume"],
2152                            data["blocked"],
2153                            data["currency"],
2154                        ) if isCurr else "{:.0f} ({:.0f})".format(
2155                            data["volume"],
2156                            data["blocked"],
2157                        ),
2158                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2159                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2160                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2161                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2162                        "{}{:.2f} {} ({}{:.2f}%)".format(
2163                            "+" if data["profit"] > 0 else "",
2164                            data["profit"], data["baseCurrencyName"],
2165                            "+" if data["percentProfit"] > 0 else "",
2166                            data["percentProfit"],
2167                        ),
2168                    )
2169
2170                # --- Show currencies section:
2171                if view["stat"]["Currencies"]:
2172                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2173                    for item in view["stat"]["Currencies"]:
2174                        info.append(_InfoStr(item, isCurr=True))
2175
2176                else:
2177                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2178
2179                # --- Show shares section:
2180                if view["stat"]["Shares"]:
2181                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2182
2183                    for item in view["stat"]["Shares"]:
2184                        info.append(_InfoStr(item))
2185
2186                else:
2187                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2188
2189                # --- Show bonds section:
2190                if view["stat"]["Bonds"]:
2191                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2192
2193                    for item in view["stat"]["Bonds"]:
2194                        info.append(_InfoStr(item))
2195
2196                else:
2197                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2198
2199                # --- Show etfs section:
2200                if view["stat"]["Etfs"]:
2201                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2202
2203                    for item in view["stat"]["Etfs"]:
2204                        info.append(_InfoStr(item))
2205
2206                else:
2207                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2208
2209                # --- Show futures section:
2210                if view["stat"]["Futures"]:
2211                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2212
2213                    for item in view["stat"]["Futures"]:
2214                        info.append(_InfoStr(item))
2215
2216                else:
2217                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2218
2219            if details in ["full", "orders"]:
2220                # --- Show pending limit orders section:
2221                if view["stat"]["orders"]:
2222                    info.extend([
2223                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2224                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2225                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2226                    ])
2227
2228                    for item in view["stat"]["orders"]:
2229                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2230                            "{} [{}]".format(item["ticker"], item["figi"]),
2231                            item["orderID"],
2232                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2233                            "{} {} ({}{:.2f}%)".format(
2234                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2235                                item["baseCurrencyName"],
2236                                "+" if item["percentChanges"] > 0 else "",
2237                                float(item["percentChanges"]),
2238                            ),
2239                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2240                            item["action"],
2241                            item["type"],
2242                            item["date"],
2243                        ))
2244
2245                else:
2246                    info.append("\n## Total pending limit-orders: [0]\n")
2247
2248                # --- Show stop orders section:
2249                if view["stat"]["stopOrders"]:
2250                    info.extend([
2251                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2252                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2253                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2254                    ])
2255
2256                    for item in view["stat"]["stopOrders"]:
2257                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2258                            "{} [{}]".format(item["ticker"], item["figi"]),
2259                            item["orderID"],
2260                            item["lotsRequested"],
2261                            "{} {} ({}{:.2f}%)".format(
2262                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2263                                item["baseCurrencyName"],
2264                                "+" if item["percentChanges"] > 0 else "",
2265                                float(item["percentChanges"]),
2266                            ),
2267                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2268                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2269                            item["action"],
2270                            item["type"],
2271                            item["expType"],
2272                            item["createDate"],
2273                            item["expDate"],
2274                        ))
2275
2276                else:
2277                    info.append("\n## Total stop-orders: [0]\n")
2278
2279            if details in ["full", "analytics"]:
2280                # -- Show analytics section:
2281                if view["stat"]["portfolioCostRUB"] > 0:
2282                    info.extend([
2283                        "\n# Analytics\n\n"
2284                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2285                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2286                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2287                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2288                            view["stat"]["totalChangesRUB"],
2289                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2290                            view["stat"]["totalChangesPercentRUB"],
2291                        ),
2292                        "\n## Portfolio distribution by assets\n"
2293                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2294                        "|------------------------------------|---------|---------|--------------------|\n",
2295                    ])
2296
2297                    for key in view["analytics"]["distrByAssets"].keys():
2298                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2299                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2300                                key,
2301                                view["analytics"]["distrByAssets"][key]["uniques"],
2302                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2303                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2304                            ))
2305
2306                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2307
2308                    info.extend([
2309                        "\n## Portfolio distribution by companies\n"
2310                        "\n| Company                                      | Percent | Current cost       |\n",
2311                        aSepLine,
2312                    ])
2313
2314                    for company in view["analytics"]["distrByCompanies"].keys():
2315                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2316                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2317                                "{}{}".format(
2318                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2319                                    company,
2320                                ),
2321                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2322                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2323                            ))
2324
2325                    info.extend([
2326                        "\n## Portfolio distribution by sectors\n"
2327                        "\n| Sector                                       | Percent | Current cost       |\n",
2328                        aSepLine,
2329                    ])
2330
2331                    for sector in view["analytics"]["distrBySectors"].keys():
2332                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2333                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2334                                sector,
2335                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2336                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2337                            ))
2338
2339                    info.extend([
2340                        "\n## Portfolio distribution by currencies\n"
2341                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2342                        aSepLine,
2343                    ])
2344
2345                    for curr in view["analytics"]["distrByCurrencies"].keys():
2346                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2347                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2348                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2349                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2350                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2351                            ))
2352
2353                    info.extend([
2354                        "\n## Portfolio distribution by countries\n"
2355                        "\n| Assets by country                            | Percent | Current cost       |\n",
2356                        aSepLine,
2357                    ])
2358
2359                    for country in view["analytics"]["distrByCountries"].keys():
2360                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2361                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2362                                country,
2363                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2364                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2365                            ))
2366
2367            if details in ["full", "calendar"]:
2368                # -- Show bonds payment calendar section:
2369                if view["stat"]["Bonds"]:
2370                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2371                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2372                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2373
2374                else:
2375                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2376
2377            infoText = "".join(info)
2378
2379            uLogger.info(infoText)
2380
2381            if details == "full" and self.overviewFile:
2382                filename = self.overviewFile
2383
2384            elif details == "digest" and self.overviewDigestFile:
2385                filename = self.overviewDigestFile
2386
2387            elif details == "positions" and self.overviewPositionsFile:
2388                filename = self.overviewPositionsFile
2389
2390            elif details == "orders" and self.overviewOrdersFile:
2391                filename = self.overviewOrdersFile
2392
2393            elif details == "analytics" and self.overviewAnalyticsFile:
2394                filename = self.overviewAnalyticsFile
2395
2396            elif details == "calendar" and self.overviewBondsCalendarFile:
2397                filename = self.overviewBondsCalendarFile
2398
2399            else:
2400                filename = ""
2401
2402            if filename:
2403                with open(filename, "w", encoding="UTF-8") as fH:
2404                    fH.write(infoText)
2405
2406                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2407
2408                if self.useHTMLReports:
2409                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2410                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2411                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2412
2413                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2414
2415        return view
2416
2417    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2418        """
2419        Returns history operations between two given dates for current `accountId`.
2420        If `reportFile` string is not empty then also save human-readable report.
2421        Shows some statistical data of closed positions.
2422
2423        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2424        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2425        :param show: if `True` then also prints all records to the console.
2426        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2427        :return: original list of dictionaries with history of deals records from API ("operations" key):
2428                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2429                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2430        """
2431        if self.accountId is None or not self.accountId:
2432            uLogger.error("Variable `accountId` must be defined for using this method!")
2433            raise Exception("Account ID required")
2434
2435        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2436
2437        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2438
2439        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2440        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2441        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2442        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2443        customStat = {}  # custom statistics in additional to responseJSON
2444
2445        # --- output report in human-readable format:
2446        if show or self.reportFile:
2447            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2448            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2449            nextDay = ""
2450
2451            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2452
2453            if len(ops) > 0:
2454                customStat = {
2455                    "opsCount": 0,  # total operations count
2456                    "buyCount": 0,  # buy operations
2457                    "sellCount": 0,  # sell operations
2458                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2459                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2460                    "payIn": {"rub": 0.},  # Deposit brokerage account
2461                    "payOut": {"rub": 0.},  # Withdrawals
2462                    "divs": {"rub": 0.},  # Dividends income
2463                    "coupons": {"rub": 0.},  # Coupon's income
2464                    "brokerCom": {"rub": 0.},  # Service commissions
2465                    "serviceCom": {"rub": 0.},  # Service commissions
2466                    "marginCom": {"rub": 0.},  # Margin commissions
2467                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2468                }
2469
2470                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2471                for item in ops:
2472                    if item["state"] == "OPERATION_STATE_EXECUTED":
2473                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2474
2475                        # count buy operations:
2476                        if "_BUY" in item["operationType"]:
2477                            customStat["buyCount"] += 1
2478
2479                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2480                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2481
2482                            else:
2483                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2484
2485                        # count sell operations:
2486                        elif "_SELL" in item["operationType"]:
2487                            customStat["sellCount"] += 1
2488
2489                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2490                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2491
2492                            else:
2493                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2494
2495                        # count incoming operations:
2496                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2497                            if item["payment"]["currency"] in customStat["payIn"].keys():
2498                                customStat["payIn"][item["payment"]["currency"]] += payment
2499
2500                            else:
2501                                customStat["payIn"][item["payment"]["currency"]] = payment
2502
2503                        # count withdrawals operations:
2504                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2505                            if item["payment"]["currency"] in customStat["payOut"].keys():
2506                                customStat["payOut"][item["payment"]["currency"]] += payment
2507
2508                            else:
2509                                customStat["payOut"][item["payment"]["currency"]] = payment
2510
2511                        # count dividends income:
2512                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2513                            if item["payment"]["currency"] in customStat["divs"].keys():
2514                                customStat["divs"][item["payment"]["currency"]] += payment
2515
2516                            else:
2517                                customStat["divs"][item["payment"]["currency"]] = payment
2518
2519                        # count coupon's income:
2520                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2521                            if item["payment"]["currency"] in customStat["coupons"].keys():
2522                                customStat["coupons"][item["payment"]["currency"]] += payment
2523
2524                            else:
2525                                customStat["coupons"][item["payment"]["currency"]] = payment
2526
2527                        # count broker commissions:
2528                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2529                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2530                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2531
2532                            else:
2533                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2534
2535                        # count service commissions:
2536                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2537                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2538                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2539
2540                            else:
2541                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2542
2543                        # count margin commissions:
2544                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2545                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2546                                customStat["marginCom"][item["payment"]["currency"]] += payment
2547
2548                            else:
2549                                customStat["marginCom"][item["payment"]["currency"]] = payment
2550
2551                        # count withholding taxes:
2552                        elif "_TAX" in item["operationType"]:
2553                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2554                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2555
2556                            else:
2557                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2558
2559                        else:
2560                            continue
2561
2562                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2563
2564                # --- view "Actions" lines:
2565                info.extend([
2566                    "| Report sections            |                               |                              |                      |                        |\n",
2567                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2568                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2569                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2570                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2571                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2572                    ),
2573                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2574                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2575                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2576                    ),
2577                ])
2578
2579                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2580                for key in opsKeys:
2581                    if key == "rub":
2582                        continue
2583
2584                    info.extend([
2585                        "|                            |                               | {:<28} |                      |                        |\n".format(
2586                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2587                        ),
2588                        "|                            |                               | {:<28} |                      |                        |\n".format(
2589                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2590                        ),
2591                    ])
2592
2593                info.append(splitLine1)
2594
2595                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2596                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2597                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2598                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2599                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2600                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2601                    )
2602
2603                # --- view "Payments" lines:
2604                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2605                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2606
2607                for key in paymentsKeys:
2608                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2609
2610                info.append(splitLine1)
2611
2612                # --- view "Commissions and taxes" lines:
2613                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2614                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2615
2616                for key in comKeys:
2617                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2618
2619                info.extend([
2620                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2621                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2622                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2623                ])
2624
2625            else:
2626                info.append("Broker returned no operations during this period\n")
2627
2628            # --- view "Operations" section:
2629            for item in ops:
2630                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2631                    continue
2632
2633                else:
2634                    self._figi = item["figi"]
2635                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2636                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2637
2638                    # group of deals during one day:
2639                    if nextDay and item["date"].split("T")[0] != nextDay:
2640                        info.append(splitLine2)
2641                        nextDay = ""
2642
2643                    else:
2644                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2645
2646                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2647                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2648                        self._figi if self._figi else "—",
2649                        instrument["ticker"] if instrument else "—",
2650                        instrument["type"] if instrument else "—",
2651                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2652                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2653                        TKS_OPERATION_STATES[item["state"]],
2654                        TKS_OPERATION_TYPES[item["operationType"]],
2655                    ))
2656
2657            infoText = "".join(info)
2658
2659            if show:
2660                if self.moreDebug:
2661                    uLogger.debug("Records about history of a client's operations successfully received")
2662
2663                uLogger.info(infoText)
2664
2665            if self.reportFile:
2666                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2667                    fH.write(infoText)
2668
2669                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2670
2671                if self.useHTMLReports:
2672                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2673                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2674                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2675
2676                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2677
2678        return ops, customStat
2679
2680    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2681        """
2682        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2683
2684        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2685        Warning! Broker server used ISO UTC time by default.
2686
2687        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2688        Also, `historyFile` used to update history with `onlyMissing` parameter.
2689
2690        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2691
2692        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2693        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2694        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2695                         `"hour"`, `"day"`. Default: `"hour"`.
2696        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2697                            False by default. Warning! History appends only from last candle to current time
2698                            with always update last candle!
2699        :param csvSep: separator if csv-file is used, `,` by default.
2700        :param show: if `True` then also prints Pandas DataFrame to the console.
2701        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2702                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2703        """
2704        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2705        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2706        history = None  # empty pandas object for history
2707
2708        if interval not in TKS_CANDLE_INTERVALS.keys():
2709            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2710            raise Exception("Incorrect value")
2711
2712        if not (self._ticker or self._figi):
2713            uLogger.error("Ticker or FIGI must be defined!")
2714            raise Exception("Ticker or FIGI required")
2715
2716        if self._ticker and not self._figi:
2717            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2718            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2719
2720        if self._figi and not self._ticker:
2721            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2722            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2723
2724        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2725        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2726        if interval.lower() != "day":
2727            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2728
2729        delta = dtEnd - dtStart  # current UTC time minus last time in file
2730        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2731
2732        # calculate history length in candles:
2733        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2734        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2735            length += 1  # to avoid fraction time
2736
2737        # calculate data blocks count:
2738        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2739
2740        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2741        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2742        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2743        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2744        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2745
2746        tempOld = None  # pandas object for old history, if --only-missing key present
2747        lastTime = None  # datetime object of last old candle in file
2748
2749        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2750            uLogger.debug("--only-missing key present, add only last missing candles...")
2751            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2752
2753            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2754
2755            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2756            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2757            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2758            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2759
2760            # get last datetime object from last string in file or minus 1 delta if file is empty:
2761            if len(tempOld) > 0:
2762                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2763
2764            else:
2765                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2766
2767            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2768
2769        responseJSONs = []  # raw history blocks of data
2770
2771        blockEnd = dtEnd
2772        for item in range(blocks):
2773            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2774            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2775
2776            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2777                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2778            ))
2779
2780            if blockStart == blockEnd:
2781                uLogger.debug("Skipped this zero-length block...")
2782
2783            else:
2784                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2785                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2786                self.body = str({
2787                    "figi": self._figi,
2788                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2789                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2790                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2791                })
2792                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2793
2794                if "code" in responseJSON.keys():
2795                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2796
2797                else:
2798                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2799                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2800
2801                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2802
2803            blockEnd = blockStart
2804
2805        printCount = len(responseJSONs)  # candles to show in console
2806        if responseJSONs:
2807            tempHistory = pd.DataFrame(
2808                data={
2809                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2810                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2811                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2812                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2813                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2814                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2815                    "volume": [int(item["volume"]) for item in responseJSONs],
2816                },
2817                index=range(len(responseJSONs)),
2818                columns=["date", "time", "open", "high", "low", "close", "volume"],
2819            )
2820            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2821            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2822
2823            # append only newest candles to old history if --only-missing key present:
2824            if onlyMissing and tempOld is not None and lastTime is not None:
2825                index = 0  # find start index in tempHistory data:
2826
2827                for i, item in tempHistory.iterrows():
2828                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2829
2830                    if curTime == lastTime:
2831                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2832                        index = i
2833                        printCount = index + 1
2834                        break
2835
2836                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2837
2838            else:
2839                history = tempHistory  # if no `--only-missing` key then load full data from server
2840
2841            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2842
2843        if history is not None and not history.empty:
2844            if show:
2845                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2846                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2847                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2848                ))
2849
2850        else:
2851            uLogger.warning("Received an empty candles history!")
2852
2853        if self.historyFile is not None:
2854            if history is not None and not history.empty:
2855                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2856                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2857
2858            else:
2859                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2860
2861        else:
2862            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2863
2864        return history
2865
2866    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2867        """
2868        Load candles history from csv-file and return Pandas DataFrame object.
2869
2870        See also: `History()` and `ShowHistoryChart()` methods.
2871
2872        :param filePath: path to csv-file to open.
2873        """
2874        loadedHistory = None  # init candles data object
2875
2876        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2877
2878        if os.path.exists(filePath):
2879            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2880
2881            tfStr = self.priceModel.FormattedDelta(
2882                self.priceModel.timeframe,
2883                "{days} days {hours}h {minutes}m {seconds}s",
2884            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2885                self.priceModel.timeframe,
2886                "{hours}h {minutes}m {seconds}s",
2887            )
2888
2889            if loadedHistory is not None and not loadedHistory.empty:
2890                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2891                    len(loadedHistory),
2892                    tfStr,
2893                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2894                )
2895
2896            else:
2897                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2898
2899        else:
2900            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2901
2902        return loadedHistory
2903
2904    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2905        """
2906        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2907
2908        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2909        Default: `index.html` (both for interact and non-interact candlesticks chart).
2910
2911        See also: `History()` and `LoadHistory()` methods.
2912
2913        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2914        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2915                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2916                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2917                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2918        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2919                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2920        """
2921        if isinstance(candles, str):
2922            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2923            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2924
2925        elif isinstance(candles, pd.DataFrame):
2926            self.priceModel.prices = candles  # set candles chain from variable
2927            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2928
2929            if "datetime" not in candles.columns:
2930                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2931
2932        else:
2933            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2934            raise Exception("Incorrect value")
2935
2936        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2937
2938        if interact:
2939            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2940
2941            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2942
2943        else:
2944            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2945
2946            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2947
2948        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2949
2950    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2951        """
2952        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2953        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2954
2955        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2956
2957        :param operation: string "Buy" or "Sell".
2958        :param lots: volume, integer count of lots >= 1.
2959        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2960        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2961        :param expDate: string "Undefined" by default or local date in future,
2962                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2963        :return: JSON with response from broker server.
2964        """
2965        if self.accountId is None or not self.accountId:
2966            uLogger.error("Variable `accountId` must be defined for using this method!")
2967            raise Exception("Account ID required")
2968
2969        if operation is None or not operation or operation not in ("Buy", "Sell"):
2970            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2971            raise Exception("Incorrect value")
2972
2973        if lots is None or lots < 1:
2974            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2975            lots = 1
2976
2977        if tp is None or tp < 0:
2978            tp = 0
2979
2980        if sl is None or sl < 0:
2981            sl = 0
2982
2983        if expDate is None or not expDate:
2984            expDate = "Undefined"
2985
2986        if not (self._ticker or self._figi):
2987            uLogger.error("Ticker or FIGI must be defined!")
2988            raise Exception("Ticker or FIGI required")
2989
2990        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2991        self._ticker = instrument["ticker"]
2992        self._figi = instrument["figi"]
2993
2994        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
2995
2996        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2997        self.body = str({
2998            "figi": self._figi,
2999            "quantity": str(lots),
3000            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3001            "accountId": str(self.accountId),
3002            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3003        })
3004        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3005
3006        if "orderId" in response.keys():
3007            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3008                operation, response["orderId"],
3009                self._ticker, self._figi, lots,
3010                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3011                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3012                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3013            ))
3014
3015            if tp > 0:
3016                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3017
3018            if sl > 0:
3019                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3020
3021        else:
3022            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3023
3024        return response
3025
3026    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3027        """
3028        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3029        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3030
3031        See also: `Order()` and `Trade()` docstrings.
3032
3033        :param lots: volume, integer count of lots >= 1.
3034        :param tp: float > 0, take profit price of stop-order.
3035        :param sl: float > 0, stop loss price of stop-order.
3036        :param expDate: it's a local date in future.
3037                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3038        :return: JSON with response from broker server.
3039        """
3040        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3041
3042    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3043        """
3044        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3045        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3046
3047        See also: `Order()` and `Trade()` docstrings.
3048
3049        :param lots: volume, integer count of lots >= 1.
3050        :param tp: float > 0, take profit price of stop-order.
3051        :param sl: float > 0, stop loss price of stop-order.
3052        :param expDate: it's a local date in the future.
3053                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3054        :return: JSON with response from broker server.
3055        """
3056        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3057
3058    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3059        """
3060        Close position of given instruments.
3061
3062        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3063        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3064                         This avoids unnecessary downloading data from the server.
3065        """
3066        if instruments is None or not instruments:
3067            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3068            raise Exception("Ticker or FIGI required")
3069
3070        if isinstance(instruments, str):
3071            instruments = [instruments]
3072
3073        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3074        if uniqueInstruments:
3075            if portfolio is None or not portfolio:
3076                portfolio = self.Overview(show=False)
3077
3078            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3079            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3080
3081            for self._figi in uniqueInstruments:
3082                if self._figi not in allOpened:
3083                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3084                    continue
3085
3086                # search open trade info about instrument by ticker:
3087                instrument = {}
3088                for iType in TKS_INSTRUMENTS:
3089                    if instrument:
3090                        break
3091
3092                    for item in portfolio["stat"][iType]:
3093                        if item["figi"] == self._figi:
3094                            instrument = item
3095                            break
3096
3097                if instrument:
3098                    self._ticker = instrument["ticker"]
3099                    self._figi = instrument["figi"]
3100
3101                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3102                        self._ticker,
3103                        self._figi,
3104                        int(instrument["volume"]),
3105                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3106                    ))
3107
3108                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3109
3110                    if tradeLots > 0:
3111                        if instrument["blocked"] > 0:
3112                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3113                                instrument["blocked"],
3114                                self._ticker,
3115                                tradeLots,
3116                            ))
3117
3118                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3119                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3120
3121                    else:
3122                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3123
3124    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3125        """
3126        Close all positions of given instruments with defined type.
3127
3128        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3129        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3130                         This avoids unnecessary downloading data from the server.
3131        """
3132        if iType not in TKS_INSTRUMENTS:
3133            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3134
3135        else:
3136            if portfolio is None or not portfolio:
3137                portfolio = self.Overview(show=False)
3138
3139            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3140            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3141
3142            if tickers and portfolio:
3143                self.CloseTrades(tickers, portfolio)
3144
3145            else:
3146                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3147
3148    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3149        """
3150        Universal method to create market or limit orders with all available parameters for current `accountId`.
3151        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3152
3153        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3154        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3155
3156        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3157        then broker immediately open market order as you can do simple --buy or --sell operations!
3158
3159        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3160        When current price will go up or down to target price value then broker opens a limit order.
3161        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3162
3163        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3164
3165        :param operation: string "Buy" or "Sell".
3166        :param orderType: string "Limit" or "Stop".
3167        :param lots: volume, integer count of lots >= 1.
3168        :param targetPrice: target price > 0. This is open trade price for limit order.
3169        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3170                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3171        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3172                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3173                         Stop loss order always executed by market price.
3174        :param expDate: string "Undefined" by default or local date in future.
3175                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3176                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3177                        A limit order has no expiration date, it lasts until the end of the trading day.
3178        :return: JSON with response from broker server.
3179        """
3180        if self.accountId is None or not self.accountId:
3181            uLogger.error("Variable `accountId` must be defined for using this method!")
3182            raise Exception("Account ID required")
3183
3184        if operation is None or not operation or operation not in ("Buy", "Sell"):
3185            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3186            raise Exception("Incorrect value")
3187
3188        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3189            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3190            raise Exception("Incorrect value")
3191
3192        if lots is None or lots < 1:
3193            uLogger.error("You must define trade volume > 0: integer count of lots!")
3194            raise Exception("Incorrect value")
3195
3196        if targetPrice is None or targetPrice <= 0:
3197            uLogger.error("Target price for limit-order must be greater than 0!")
3198            raise Exception("Incorrect value")
3199
3200        if limitPrice is None or limitPrice <= 0:
3201            limitPrice = targetPrice
3202
3203        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3204            stopType = "Limit"
3205
3206        if expDate is None or not expDate:
3207            expDate = "Undefined"
3208
3209        if not (self._ticker or self._figi):
3210            uLogger.error("Tocker or FIGI must be defined!")
3211            raise Exception("Ticker or FIGI required")
3212
3213        response = {}
3214        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3215        self._ticker = instrument["ticker"]
3216        self._figi = instrument["figi"]
3217
3218        if orderType == "Limit":
3219            uLogger.debug(
3220                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3221                    self._ticker, self._figi,
3222                    operation, lots, targetPrice, instrument["currency"],
3223                ))
3224
3225            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3226            self.body = str({
3227                "figi": self._figi,
3228                "quantity": str(lots),
3229                "price": FloatToNano(targetPrice),
3230                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3231                "accountId": str(self.accountId),
3232                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3233            })
3234            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3235
3236            if "orderId" in response.keys():
3237                uLogger.info(
3238                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3239                        response["orderId"], self._ticker, self._figi, operation, lots,
3240                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3241                    ))
3242
3243                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3244                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3245                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3246                            targetPrice, instrument["currency"],
3247                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3248                        ))
3249
3250                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3251                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3252                            targetPrice, instrument["currency"],
3253                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3254                        ))
3255
3256            else:
3257                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3258
3259        if orderType == "Stop":
3260            uLogger.debug(
3261                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3262                    self._ticker, self._figi,
3263                    operation, lots,
3264                    targetPrice, instrument["currency"],
3265                    limitPrice, instrument["currency"],
3266                    stopType, expDate,
3267                ))
3268
3269            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3270            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3271            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3272
3273            body = {
3274                "figi": self._figi,
3275                "quantity": str(lots),
3276                "price": FloatToNano(limitPrice),
3277                "stopPrice": FloatToNano(targetPrice),
3278                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3279                "accountId": str(self.accountId),
3280                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3281                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3282            }
3283
3284            if expDateUTC:
3285                body["expireDate"] = expDateUTC
3286
3287            self.body = str(body)
3288            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3289
3290            if "stopOrderId" in response.keys():
3291                uLogger.info(
3292                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3293                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3294                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3295                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3296                        TKS_STOP_ORDER_TYPES[stopOrderType],
3297                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3298                    ))
3299
3300                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3301                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3302                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3303                            targetPrice, instrument["currency"],
3304                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3305                        ))
3306
3307                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3308                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3309                            targetPrice, instrument["currency"],
3310                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3311                        ))
3312
3313            else:
3314                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3315
3316        return response
3317
3318    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3319        """
3320        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3321        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3322        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3323        See also: `Order()` docstring.
3324
3325        :param lots: volume, integer count of lots >= 1.
3326        :param targetPrice: target price > 0. This is open trade price for limit order.
3327        :return: JSON with response from broker server.
3328        """
3329        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3330
3331    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3332        """
3333        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3334        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3335        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3336        target price value then broker opens a limit order. See also: `Order()` docstring.
3337
3338        :param lots: volume, integer count of lots >= 1.
3339        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3340        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3341                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3342        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3343                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3344        :param expDate: string "Undefined" by default or local date in future.
3345                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3346                        This date is converting to UTC format for server.
3347        :return: JSON with response from broker server.
3348        """
3349        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3350
3351    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3352        """
3353        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3354        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3355        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3356        See also: `Order()` docstring.
3357
3358        :param lots: volume, integer count of lots >= 1.
3359        :param targetPrice: target price > 0. This is open trade price for limit order.
3360        :return: JSON with response from broker server.
3361        """
3362        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3363
3364    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3365        """
3366        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3367        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3368        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3369        target price value then broker opens a limit order. See also: `Order()` docstring.
3370
3371        :param lots: volume, integer count of lots >= 1.
3372        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3373        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3374                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3375        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3376                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3377        :param expDate: string "Undefined" by default or local date in future.
3378                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3379                        This date is converting to UTC format for server.
3380        :return: JSON with response from broker server.
3381        """
3382        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3383
3384    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3385        """
3386        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3387
3388        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3389        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3390                             This avoids unnecessary downloading data from the server.
3391        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3392        """
3393        if self.accountId is None or not self.accountId:
3394            uLogger.error("Variable `accountId` must be defined for using this method!")
3395            raise Exception("Account ID required")
3396
3397        if orderIDs:
3398            if allOrdersIDs is None:
3399                rawOrders = self.RequestPendingOrders()
3400                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3401
3402            if allStopOrdersIDs is None:
3403                rawStopOrders = self.RequestStopOrders()
3404                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3405
3406            for orderID in orderIDs:
3407                idInPendingOrders = orderID in allOrdersIDs
3408                idInStopOrders = orderID in allStopOrdersIDs
3409
3410                if not (idInPendingOrders or idInStopOrders):
3411                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3412                    continue
3413
3414                else:
3415                    if idInPendingOrders:
3416                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3417
3418                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3419                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3420                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3421                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3422
3423                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3424                            if self.moreDebug:
3425                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3426
3427                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3428
3429                        else:
3430                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3431
3432                    elif idInStopOrders:
3433                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3434
3435                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3436                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3437                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3438                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3439
3440                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3441                            if self.moreDebug:
3442                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3443
3444                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3445
3446                        else:
3447                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3448
3449                    else:
3450                        continue
3451
3452    def CloseAllOrders(self) -> None:
3453        """
3454        Gets a list of open pending and stop orders and cancel it all.
3455        """
3456        rawOrders = self.RequestPendingOrders()
3457        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3458        lenOrders = len(allOrdersIDs)
3459
3460        rawStopOrders = self.RequestStopOrders()
3461        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3462        lenSOrders = len(allStopOrdersIDs)
3463
3464        if lenOrders > 0 or lenSOrders > 0:
3465            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3466
3467            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3468
3469        else:
3470            uLogger.info("Orders not found, nothing to cancel.")
3471
3472    def CloseAll(self, *args) -> None:
3473        """
3474        Close all available (not blocked) opened trades and orders.
3475
3476        Also, you can select one or more keywords case-insensitive:
3477        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3478
3479        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3480        """
3481        overview = self.Overview(show=False)  # get all open trades info
3482
3483        if len(args) == 0:
3484            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3485            self.CloseAllOrders()  # close all pending and stop orders
3486
3487            for iType in TKS_INSTRUMENTS:
3488                if iType != "Currencies":
3489                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3490
3491        else:
3492            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3493            lowerArgs = [x.lower() for x in args]
3494
3495            if "orders" in lowerArgs:
3496                self.CloseAllOrders()  # close all pending and stop orders
3497
3498            for iType in TKS_INSTRUMENTS:
3499                if iType.lower() in lowerArgs and iType != "Currencies":
3500                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3501
3502    def CloseAllByTicker(self, instrument: str) -> None:
3503        """
3504        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3505
3506        This method searches opened trade and orders of instrument throw all portfolio and then use
3507        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3508
3509        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3510
3511        :param instrument: string with ticker.
3512        """
3513        if instrument is None or not instrument:
3514            uLogger.error("Ticker name must be defined for using this method!")
3515            raise Exception("Ticker required")
3516
3517        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3518
3519        self._ticker = instrument  # try to set instrument as ticker
3520        self._figi = ""
3521
3522        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3523        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3524
3525        if limitAll and self.IsInLimitOrders(portfolio=overview):
3526            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3527            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3528
3529        if stopAll and self.IsInStopOrders(portfolio=overview):
3530            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3531            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3532
3533        if self.IsInPortfolio(portfolio=overview):
3534            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3535            self.CloseTrades(instruments=[instrument], portfolio=overview)
3536
3537    def CloseAllByFIGI(self, instrument: str) -> None:
3538        """
3539        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3540
3541        This method searches opened trade and orders of instrument throw all portfolio and then use
3542        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3543
3544        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3545
3546        :param instrument: string with FIGI id.
3547        """
3548        if instrument is None or not instrument:
3549            uLogger.error("FIGI id must be defined for using this method!")
3550            raise Exception("FIGI required")
3551
3552        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3553
3554        self._ticker = ""
3555        self._figi = instrument  # try to set instrument as FIGI id
3556
3557        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3558        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3559
3560        if limitAll and self.IsInLimitOrders(portfolio=overview):
3561            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3562            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3563
3564        if stopAll and self.IsInStopOrders(portfolio=overview):
3565            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3566            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3567
3568        if self.IsInPortfolio(portfolio=overview):
3569            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3570            self.CloseTrades(instruments=[instrument], portfolio=overview)
3571
3572    @staticmethod
3573    def ParseOrderParameters(operation, **inputParameters):
3574        """
3575        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3576
3577        :param operation: string "Buy" or "Sell".
3578        :param inputParameters: this is dict of strings that looks like this
3579               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3580               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3581               "prices" key: one or more prices to open limit-orders
3582               Counts of values in lots and prices lists must be equals!
3583        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3584        """
3585        # TODO: update order grid work with api v2
3586        pass
3587        # uLogger.debug("Input parameters: {}".format(inputParameters))
3588        #
3589        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3590        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3591        #     raise Exception("Incorrect value")
3592        #
3593        # if "l" in inputParameters.keys():
3594        #     inputParameters["lots"] = inputParameters.pop("l")
3595        #
3596        # if "p" in inputParameters.keys():
3597        #     inputParameters["prices"] = inputParameters.pop("p")
3598        #
3599        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3600        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3601        #     raise Exception("Incorrect value")
3602        #
3603        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3604        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3605        #
3606        # if len(lots) != len(prices):
3607        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3608        #     raise Exception("Incorrect value")
3609        #
3610        # uLogger.debug("Extracted parameters for orders:")
3611        # uLogger.debug("lots = {}".format(lots))
3612        # uLogger.debug("prices = {}".format(prices))
3613        #
3614        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3615        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3616        # uLogger.debug("Order parameters: {}".format(result))
3617        #
3618        # return result
3619
3620    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3621        """
3622        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3623
3624        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3625        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3626        """
3627        result = False
3628        msg = "Instrument not defined!"
3629
3630        if portfolio is None or not portfolio:
3631            portfolio = self.Overview(show=False)
3632
3633        if self._ticker:
3634            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3635            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3636
3637            for iType in TKS_INSTRUMENTS:
3638                for instrument in portfolio["stat"][iType]:
3639                    if instrument["ticker"] == self._ticker:
3640                        result = True
3641                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3642                        break
3643
3644        elif self._figi:
3645            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3646            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3647
3648            for iType in TKS_INSTRUMENTS:
3649                for instrument in portfolio["stat"][iType]:
3650                    if instrument["figi"] == self._figi:
3651                        result = True
3652                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3653                        break
3654
3655        else:
3656            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3657
3658        uLogger.debug(msg)
3659
3660        return result
3661
3662    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3663        """
3664        Returns instrument from the user's portfolio if it presents there.
3665        Instrument must be defined by `ticker` (highly priority) or `figi`.
3666
3667        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3668        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3669        """
3670        result = None
3671        msg = "Instrument not defined!"
3672
3673        if portfolio is None or not portfolio:
3674            portfolio = self.Overview(show=False)
3675
3676        if self._ticker:
3677            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3678            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3679
3680            for iType in TKS_INSTRUMENTS:
3681                for instrument in portfolio["stat"][iType]:
3682                    if instrument["ticker"] == self._ticker:
3683                        result = instrument
3684                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3685                        break
3686
3687        elif self._figi:
3688            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3689            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3690
3691            for iType in TKS_INSTRUMENTS:
3692                for instrument in portfolio["stat"][iType]:
3693                    if instrument["figi"] == self._figi:
3694                        result = instrument
3695                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3696                        break
3697
3698        else:
3699            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3700
3701        uLogger.debug(msg)
3702
3703        return result
3704
3705    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3706        """
3707        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3708
3709        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3710
3711        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3712        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3713        """
3714        result = False
3715        msg = "Instrument not defined!"
3716
3717        if portfolio is None or not portfolio:
3718            portfolio = self.Overview(show=False)
3719
3720        if self._ticker:
3721            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3722            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3723
3724            for instrument in portfolio["stat"]["orders"]:
3725                if instrument["ticker"] == self._ticker:
3726                    result = True
3727                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3728                    break
3729
3730        elif self._figi:
3731            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3732            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3733
3734            for instrument in portfolio["stat"]["orders"]:
3735                if instrument["figi"] == self._figi:
3736                    result = True
3737                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3738                    break
3739
3740        else:
3741            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3742
3743        uLogger.debug(msg)
3744
3745        return result
3746
3747    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3748        """
3749        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3750        Instrument must be defined by `ticker` (highly priority) or `figi`.
3751
3752        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3753
3754        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3755        :return: list with `orderID`s of limit orders.
3756        """
3757        result = []
3758        msg = "Instrument not defined!"
3759
3760        if portfolio is None or not portfolio:
3761            portfolio = self.Overview(show=False)
3762
3763        if self._ticker:
3764            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3765            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3766
3767            for instrument in portfolio["stat"]["orders"]:
3768                if instrument["ticker"] == self._ticker:
3769                    result.append(instrument["orderID"])
3770
3771            if result:
3772                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3773
3774        elif self._figi:
3775            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3776            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3777
3778            for instrument in portfolio["stat"]["orders"]:
3779                if instrument["figi"] == self._figi:
3780                    result.append(instrument["orderID"])
3781
3782            if result:
3783                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3784
3785        else:
3786            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3787
3788        uLogger.debug(msg)
3789
3790        return result
3791
3792    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3793        """
3794        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3795
3796        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3797
3798        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3799        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3800        """
3801        result = False
3802        msg = "Instrument not defined!"
3803
3804        if portfolio is None or not portfolio:
3805            portfolio = self.Overview(show=False)
3806
3807        if self._ticker:
3808            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3809            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3810
3811            for instrument in portfolio["stat"]["stopOrders"]:
3812                if instrument["ticker"] == self._ticker:
3813                    result = True
3814                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3815                    break
3816
3817        elif self._figi:
3818            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3819            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3820
3821            for instrument in portfolio["stat"]["stopOrders"]:
3822                if instrument["figi"] == self._figi:
3823                    result = True
3824                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3825                    break
3826
3827        else:
3828            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3829
3830        uLogger.debug(msg)
3831
3832        return result
3833
3834    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3835        """
3836        Returns list with all `orderID`s of opened stop orders for the instrument.
3837        Instrument must be defined by `ticker` (highly priority) or `figi`.
3838
3839        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3840
3841        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3842        :return: list with `orderID`s of stop orders.
3843        """
3844        result = []
3845        msg = "Instrument not defined!"
3846
3847        if portfolio is None or not portfolio:
3848            portfolio = self.Overview(show=False)
3849
3850        if self._ticker:
3851            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3852            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3853
3854            for instrument in portfolio["stat"]["stopOrders"]:
3855                if instrument["ticker"] == self._ticker:
3856                    result.append(instrument["orderID"])
3857
3858            if result:
3859                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3860
3861        elif self._figi:
3862            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3863            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3864
3865            for instrument in portfolio["stat"]["stopOrders"]:
3866                if instrument["figi"] == self._figi:
3867                    result.append(instrument["orderID"])
3868
3869            if result:
3870                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3871
3872        else:
3873            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3874
3875        uLogger.debug(msg)
3876
3877        return result
3878
3879    def RequestLimits(self) -> dict:
3880        """
3881        Method for obtaining the available funds for withdrawal for current `accountId`.
3882
3883        See also:
3884        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3885        - `OverviewLimits()` method
3886
3887        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3888                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3889                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3890                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3891        """
3892        if self.accountId is None or not self.accountId:
3893            uLogger.error("Variable `accountId` must be defined for using this method!")
3894            raise Exception("Account ID required")
3895
3896        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3897
3898        self.body = str({"accountId": self.accountId})
3899        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3900        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3901
3902        if self.moreDebug:
3903            uLogger.debug("Records about available funds for withdrawal successfully received")
3904
3905        return rawLimits
3906
3907    def OverviewLimits(self, show: bool = False) -> dict:
3908        """
3909        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3910
3911        See also: `RequestLimits()`.
3912
3913        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3914        :return: dict with raw parsed data from server and some calculated statistics about it.
3915        """
3916        if self.accountId is None or not self.accountId:
3917            uLogger.error("Variable `accountId` must be defined for using this method!")
3918            raise Exception("Account ID required")
3919
3920        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3921
3922        view = {
3923            "rawLimits": rawLimits,
3924            "limits": {  # parsed data for every currency:
3925                "money": {  # this is an array of portfolio currency positions
3926                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3927                },
3928                "blocked": {  # this is an array of blocked currency
3929                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3930                },
3931                "blockedGuarantee": {  # this is locked money under collateral for futures
3932                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3933                },
3934            },
3935        }
3936
3937        # --- Prepare text table with limits in human-readable format:
3938        if show:
3939            info = [
3940                "# Withdrawal limits\n\n",
3941                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3942                "* **Account ID:** [{}]\n".format(self.accountId),
3943            ]
3944
3945            if view["limits"]["money"]:
3946                info.extend([
3947                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3948                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3949                ])
3950
3951            else:
3952                info.append("\nNo withdrawal limits\n")
3953
3954            for curr in view["limits"]["money"].keys():
3955                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3956                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3957                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3958
3959                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3960                    "[{}]".format(curr),
3961                    "{:.2f}".format(view["limits"]["money"][curr]),
3962                    "{:.2f}".format(availableMoney),
3963                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3964                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3965                )
3966
3967                if curr == "rub":
3968                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3969
3970                else:
3971                    info.append(infoStr)
3972
3973            infoText = "".join(info)
3974
3975            uLogger.info(infoText)
3976
3977            if self.withdrawalLimitsFile:
3978                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3979                    fH.write(infoText)
3980
3981                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3982
3983                if self.useHTMLReports:
3984                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3985                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3986                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
3987
3988                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
3989
3990        return view
3991
3992    def RequestAccounts(self) -> dict:
3993        """
3994        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3995
3996        See also:
3997        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3998        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
3999        - `OverviewUserInfo()` method
4000
4001        :return: dict with raw data from server that contains accounts info. Example of dict:
4002                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4003                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4004                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4005                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4006        """
4007        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4008
4009        self.body = str({})
4010        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4011        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4012
4013        if self.moreDebug:
4014            uLogger.debug("Records about available accounts successfully received")
4015
4016        return rawAccounts
4017
4018    def RequestUserInfo(self) -> dict:
4019        """
4020        Method for requesting common user's information.
4021
4022        See also:
4023        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4024        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4025        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4026        - `OverviewUserInfo()` method
4027
4028        :return: dict with raw data from server that contains user's information. Example of dict:
4029                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4030                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4031        """
4032        uLogger.debug("Requesting common user's information. Wait, please...")
4033
4034        self.body = str({})
4035        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4036        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4037
4038        if self.moreDebug:
4039            uLogger.debug("Records about current user successfully received")
4040
4041        return rawUserInfo
4042
4043    def RequestMarginStatus(self, accountId: str = None) -> dict:
4044        """
4045        Method for requesting margin calculation for defined account ID.
4046
4047        See also:
4048        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4049        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4050        - `OverviewUserInfo()` method
4051
4052        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4053        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4054                 Example of responses:
4055                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4056                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4057                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4058                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4059                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4060                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4061        """
4062        if accountId is None or not accountId:
4063            if self.accountId is None or not self.accountId:
4064                uLogger.error("Variable `accountId` must be defined for using this method!")
4065                raise Exception("Account ID required")
4066
4067            else:
4068                accountId = self.accountId  # use `self.accountId` (main ID) by default
4069
4070        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4071
4072        self.body = str({"accountId": accountId})
4073        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4074        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4075
4076        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4077            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4078            rawMargin = {}
4079
4080        else:
4081            if self.moreDebug:
4082                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4083
4084        return rawMargin
4085
4086    def RequestTariffLimits(self) -> dict:
4087        """
4088        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4089
4090        See also:
4091        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4092        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4093        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4094        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4095        - `OverviewUserInfo()` method
4096
4097        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4098                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4099                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4100        """
4101        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4102
4103        self.body = str({})
4104        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4105        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4106
4107        if self.moreDebug:
4108            uLogger.debug("Records with limits of current tariff successfully received")
4109
4110        return rawTariffLimits
4111
4112    def RequestBondCoupons(self, iJSON: dict) -> dict:
4113        """
4114        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4115        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4116        All dates are in UTC timezone.
4117
4118        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4119        Documentation:
4120        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4121        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4122
4123        See also: `ExtendBondsData()`.
4124
4125        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4126                      If raw iJSON is not data of bond then server returns an error [400] with message:
4127                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4128        :return: dictionary with bond payment calendar. Response example
4129                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4130                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4131                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4132                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4133        """
4134        if iJSON["figi"] is None or not iJSON["figi"]:
4135            uLogger.error("FIGI must be defined for using this method!")
4136            raise Exception("FIGI required")
4137
4138        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4139        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4140
4141        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4142            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4143            self._figi,
4144            startDate,
4145            endDate,
4146        ))
4147
4148        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4149        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4150        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4151
4152        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4153            uLogger.warning("Instrument type is not bond!")
4154
4155        else:
4156            if self.moreDebug:
4157                uLogger.debug("Records about bond payment calendar successfully received")
4158
4159        return calendar
4160
4161    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4162        """
4163        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4164        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4165        coupon yields, current yields and some statistics etc.
4166
4167        WARNING! This is too long operation if a lot of bonds requested from broker server.
4168
4169        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4170
4171        :param instruments: list of strings with tickers or FIGIs.
4172        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4173                     for further used by data scientists or stock analytics.
4174        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4175                 In XLSX-file and Pandas DataFrame fields mean:
4176                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4177                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4178        """
4179        if instruments is None or not instruments:
4180            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4181            raise Exception("Ticker or FIGI required")
4182
4183        if isinstance(instruments, str):
4184            instruments = [instruments]
4185
4186        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4187
4188        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4189
4190        iCount = len(uniqueInstruments)
4191        tooLong = iCount >= 20
4192        if tooLong:
4193            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4194
4195        bonds = None
4196        for i, self._figi in enumerate(uniqueInstruments):
4197            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4198
4199            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4200                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4201                rawBond = self.SearchByFIGI(requestPrice=True)
4202
4203                # Widen raw data with UTC current time (iData["actualDateTime"]):
4204                actualDate = datetime.now(tzutc())
4205                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4206
4207                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4208                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4209
4210                # Replace some values with human-readable:
4211                iData["nominalCurrency"] = iData["nominal"]["currency"]
4212                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4213                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4214                iData["aciCurrency"] = iData["aciValue"]["currency"]
4215                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4216                iData["issueSize"] = int(iData["issueSize"])
4217                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4218                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4219                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4220                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4221                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4222                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4223                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4224                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4225                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4226                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4227
4228                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4229                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4230                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4231                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4232                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4233                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4234                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4235                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4236                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4237                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4238                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4239
4240                # Widen raw data with calendar data from `rawCalendar` values:
4241                calendarData = []
4242                if "events" in iData["rawCalendar"].keys():
4243                    for item in iData["rawCalendar"]["events"]:
4244                        calendarData.append({
4245                            "couponDate": item["couponDate"],
4246                            "couponNumber": int(item["couponNumber"]),
4247                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4248                            "payCurrency": item["payOneBond"]["currency"],
4249                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4250                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4251                            "couponStartDate": item["couponStartDate"],
4252                            "couponEndDate": item["couponEndDate"],
4253                            "couponPeriod": item["couponPeriod"],
4254                        })
4255
4256                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4257                    if "maturityDate" not in iData.keys():
4258                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4259
4260                # Widen raw data with Coupon Rate.
4261                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4262                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4263                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4264                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4265
4266                # Widen raw data with Yield to Maturity (YTM) on current date.
4267                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4268                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4269                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4270                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4271                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4272                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4273
4274                iData["calendar"] = calendarData  # adds calendar at the end
4275
4276                # Remove not used data:
4277                iData.pop("uid")
4278                iData.pop("positionUid")
4279                iData.pop("currentPrice")
4280                iData.pop("rawCalendar")
4281
4282                colNames = list(iData.keys())
4283                if bonds is None:
4284                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4285
4286                else:
4287                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4288
4289            else:
4290                uLogger.warning("Instrument is not a bond!")
4291
4292            processed = round(100 * (i + 1) / iCount, 1)
4293            if tooLong and processed % 5 == 0:
4294                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4295
4296            else:
4297                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4298
4299        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4300
4301        # Saving bonds from Pandas DataFrame to XLSX sheet:
4302        if xlsx and self.bondsXLSXFile:
4303            with pd.ExcelWriter(
4304                    path=self.bondsXLSXFile,
4305                    date_format=TKS_DATE_FORMAT,
4306                    datetime_format=TKS_DATE_TIME_FORMAT,
4307                    mode="w",
4308            ) as writer:
4309                bonds.to_excel(
4310                    writer,
4311                    sheet_name="Extended bonds data",
4312                    index=True,
4313                    encoding="UTF-8",
4314                    freeze_panes=(1, 1),
4315                )  # saving as XLSX-file with freeze first row and column as headers
4316
4317            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4318
4319        return bonds
4320
4321    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4322        """
4323        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4324
4325        WARNING! This is too long operation if a lot of bonds requested from broker server.
4326
4327        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4328
4329        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4330                        extended information about bonds: main info, current prices, bond payment calendar,
4331                        coupon yields, current yields and some statistics etc.
4332                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4333        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4334                     for further used by data scientists or stock analytics.
4335        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4336        """
4337        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4338            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4339
4340        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4341
4342        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4343        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4344        calendar = None
4345        for bond in extBonds.iterrows():
4346            for item in bond[1]["calendar"]:
4347                cData = {
4348                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4349                    "couponDate": item["couponDate"],
4350                    "figi": bond[1]["figi"],
4351                    "ticker": bond[1]["ticker"],
4352                    "name": bond[1]["name"],
4353                    "couponNumber": item["couponNumber"],
4354                    "payOneBond": item["payOneBond"],
4355                    "payCurrency": item["payCurrency"],
4356                    "couponType": item["couponType"],
4357                    "couponPeriod": item["couponPeriod"],
4358                    "fixDate": item["fixDate"],
4359                    "couponStartDate": item["couponStartDate"],
4360                    "couponEndDate": item["couponEndDate"],
4361                }
4362
4363                if calendar is None:
4364                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4365
4366                else:
4367                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4368
4369        if calendar is not None:
4370            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4371
4372            # Saving calendar from Pandas DataFrame to XLSX sheet:
4373            if xlsx:
4374                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4375
4376                with pd.ExcelWriter(
4377                        path=xlsxCalendarFile,
4378                        date_format=TKS_DATE_FORMAT,
4379                        datetime_format=TKS_DATE_TIME_FORMAT,
4380                        mode="w",
4381                ) as writer:
4382                    humanReadable = calendar.copy(deep=True)
4383                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4384                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4385                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4386                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4387                    humanReadable.columns = colNames  # human-readable column names
4388
4389                    humanReadable.to_excel(
4390                        writer,
4391                        sheet_name="Bond payments calendar",
4392                        index=False,
4393                        encoding="UTF-8",
4394                        freeze_panes=(1, 2),
4395                    )  # saving as XLSX-file with freeze first row and column as headers
4396
4397                    del humanReadable  # release df in memory
4398
4399                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4400
4401        return calendar
4402
4403    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4404        """
4405        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4406        Also, creates Markdown file with calendar data, `calendar.md` by default.
4407
4408        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4409
4410        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4411                        extended information about bonds: main info, current prices, bond payment calendar,
4412                        coupon yields, current yields and some statistics etc.
4413                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4414        :param show: if `True` then also printing bonds payment calendar to the console,
4415                     otherwise save to file `calendarFile` only. `False` by default.
4416        :return: multilines text in Markdown format with bonds payment calendar as a table.
4417        """
4418        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4419            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4420
4421        infoText = "# Bond payments calendar\n\n"
4422
4423        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4424
4425        if not (calendar is None or calendar.empty):
4426            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4427
4428            info = [
4429                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4430                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4431                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4432            ]
4433
4434            newMonth = False
4435            notOneBond = calendar["figi"].nunique() > 1
4436            for i, bond in enumerate(calendar.iterrows()):
4437                if newMonth and notOneBond:
4438                    info.append(splitLine)
4439
4440                info.append(
4441                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4442                        "  √" if bond[1]["paid"] else "  —",
4443                        bond[1]["couponDate"].split("T")[0],
4444                        bond[1]["figi"],
4445                        bond[1]["ticker"],
4446                        bond[1]["couponNumber"],
4447                        "{} {}".format(
4448                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4449                            bond[1]["payCurrency"],
4450                        ),
4451                        bond[1]["couponType"],
4452                        bond[1]["couponPeriod"],
4453                        bond[1]["fixDate"].split("T")[0],
4454                    )
4455                )
4456
4457                if i < len(calendar.values) - 1:
4458                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4459                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4460                    newMonth = False if curDate.month == nextDate.month else True
4461
4462                else:
4463                    newMonth = False
4464
4465            infoText += "".join(info)
4466
4467            if show:
4468                uLogger.info("{}".format(infoText))
4469
4470            if self.calendarFile is not None:
4471                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4472                    fH.write(infoText)
4473
4474                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4475
4476                if self.useHTMLReports:
4477                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4478                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4479                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4480
4481                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4482
4483        else:
4484            infoText += "No data\n"
4485
4486        return infoText
4487
4488    def OverviewAccounts(self, show: bool = False) -> dict:
4489        """
4490        Method for parsing and show simple table with all available user accounts.
4491
4492        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4493
4494        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4495        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4496                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4497                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4498                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4499                                                        "closed": "—", "access": "Full access" }, ...}}`
4500        """
4501        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4502
4503        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4504        accounts = {
4505            item["id"]: {
4506                "type": TKS_ACCOUNT_TYPES[item["type"]],
4507                "name": item["name"],
4508                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4509                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4510                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4511                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4512            } for item in rawAccounts["accounts"]
4513        }
4514
4515        # Raw and parsed data with some fields replaced in "stat" section:
4516        view = {
4517            "rawAccounts": rawAccounts,
4518            "stat": accounts,
4519        }
4520
4521        # --- Prepare simple text table with only accounts data in human-readable format:
4522        if show:
4523            info = [
4524                "# User accounts\n\n",
4525                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4526                "| Account ID   | Type                      | Status                    | Name                           |\n",
4527                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4528            ]
4529
4530            for account in view["stat"].keys():
4531                info.extend([
4532                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4533                        account,
4534                        view["stat"][account]["type"],
4535                        view["stat"][account]["status"],
4536                        view["stat"][account]["name"],
4537                    )
4538                ])
4539
4540            infoText = "".join(info)
4541
4542            uLogger.info(infoText)
4543
4544            if self.userAccountsFile:
4545                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4546                    fH.write(infoText)
4547
4548                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4549
4550                if self.useHTMLReports:
4551                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4552                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4553                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4554
4555                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4556
4557        return view
4558
4559    def OverviewUserInfo(self, show: bool = False) -> dict:
4560        """
4561        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4562
4563        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4564
4565        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4566        :return: dict with raw parsed data from server and some calculated statistics about it.
4567        """
4568        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4569        tmpTicker = self._ticker
4570        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4571        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4572        self._ticker = tmpTicker
4573
4574        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4575        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4576        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4577        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4578        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4579        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4580
4581        # This is dict with parsed common user data:
4582        userInfo = {
4583            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4584            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4585            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4586            "tariff": rawUserInfo["tariff"],
4587        }
4588
4589        # This is an array of dict with parsed margin statuses for every account IDs:
4590        margins = {}
4591        for accountId in accounts.keys():
4592            if rawMargins[accountId]:
4593                margins[accountId] = {
4594                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4595                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4596                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4597                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4598                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4599                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4600                    "missing": missing["volume"],
4601                }
4602
4603            else:
4604                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4605
4606        unary = {}  # unary-connection limits
4607        for item in rawTariffLimits["unaryLimits"]:
4608            if item["limitPerMinute"] in unary.keys():
4609                unary[item["limitPerMinute"]].extend(item["methods"])
4610
4611            else:
4612                unary[item["limitPerMinute"]] = item["methods"]
4613
4614        stream = {}  # stream-connection limits
4615        for item in rawTariffLimits["streamLimits"]:
4616            if item["limit"] in stream.keys():
4617                stream[item["limit"]].extend(item["streams"])
4618
4619            else:
4620                stream[item["limit"]] = item["streams"]
4621
4622        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4623        limits = {
4624            "unary": unary,
4625            "stream": stream,
4626        }
4627
4628        # Raw and parsed data as an output result:
4629        view = {
4630            "rawUserInfo": rawUserInfo,
4631            "rawAccounts": rawAccounts,
4632            "rawMargins": rawMargins,
4633            "rawTariffLimits": rawTariffLimits,
4634            "stat": {
4635                "overview": overview,
4636                "userInfo": userInfo,
4637                "accounts": accounts,
4638                "margins": margins,
4639                "limits": limits,
4640            },
4641        }
4642
4643        # --- Prepare text table with user information in human-readable format:
4644        if show:
4645            info = [
4646                "# Full user information\n\n",
4647                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4648                "## Common information\n\n",
4649                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4650                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4651                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4652                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4653                "\n## User accounts\n\n",
4654            ]
4655
4656            for account in view["stat"]["accounts"].keys():
4657                info.extend([
4658                    "### ID: [{}]\n\n".format(account),
4659                    "| Parameters           | Values                                                       |\n",
4660                    "|----------------------|--------------------------------------------------------------|\n",
4661                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4662                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4663                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4664                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4665                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4666                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4667                ])
4668
4669                if margins[account]:
4670                    info.extend([
4671                        "| Margin status:       | Enabled                                                      |\n",
4672                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4673                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4674                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4675                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4676                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4677                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4678                    ])
4679
4680                else:
4681                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4682
4683            info.extend([
4684                "\n## Current user tariff limits\n",
4685                "\n### See also\n",
4686                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4687                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4688                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4689                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4690                "\n### Unary limits\n",
4691            ])
4692
4693            if unary:
4694                for key, values in sorted(unary.items()):
4695                    info.append("\n* Max requests per minute: {}\n".format(key))
4696
4697                    for value in values:
4698                        info.append("  - {}\n".format(value))
4699
4700            else:
4701                info.append("\nNot available\n")
4702
4703            info.append("\n### Stream limits\n")
4704
4705            if stream:
4706                for key, values in sorted(stream.items()):
4707                    info.append("\n* Max stream connections: {}\n".format(key))
4708
4709                    for value in values:
4710                        info.append("  - {}\n".format(value))
4711
4712            else:
4713                info.append("\nNot available\n")
4714
4715            infoText = "".join(info)
4716
4717            uLogger.info(infoText)
4718
4719            if self.userInfoFile:
4720                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4721                    fH.write(infoText)
4722
4723                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4724
4725                if self.useHTMLReports:
4726                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4727                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4728                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4729
4730                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4731
4732        return view
4733
4734
4735class Args:
4736    """
4737    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4738    """
4739    def __init__(self, **kwargs):
4740        self.__dict__.update(kwargs)
4741
4742    def __getattr__(self, item):
4743        return None
4744
4745
4746def ParseArgs():
4747    """This function get and parse command line keys."""
4748    parser = ArgumentParser()  # command-line string parser
4749
4750    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4751    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4752
4753    # --- options:
4754
4755    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4756    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4757    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4758
4759    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4760    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4761
4762    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4763    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4764
4765    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4766    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4767
4768    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4769    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4770    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4771
4772    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4773    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4774
4775    # --- commands:
4776
4777    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4778
4779    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4780    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4781    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4782    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4783    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4784    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4785    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4786    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4787
4788    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4789    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4790    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4791    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4792    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4793    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4794
4795    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4796    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4797    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4798    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4799
4800    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4801    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4802    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4803
4804    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4805    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4806    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4807    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4808    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4809    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4810    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4811
4812    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4813    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4814    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4815    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4816    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4817
4818    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4819    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4820    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4821
4822    cmdArgs = parser.parse_args()
4823    return cmdArgs
4824
4825
4826def Main(**kwargs):
4827    """
4828    Main function for work with TKSBrokerAPI in the console.
4829
4830    See examples:
4831    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4832    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4833    """
4834    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4835
4836    if args.debug_level:
4837        uLogger.level = 10  # always debug level by default
4838        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4839
4840    exitCode = 0
4841    start = datetime.now(tzutc())
4842    uLogger.debug("=-" * 50)
4843    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4844        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4845        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4846    ))
4847
4848    # trying to calculate full current version:
4849    buildVersion = __version__
4850    try:
4851        v = version("tksbrokerapi")
4852        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4853
4854    except Exception:
4855        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4856
4857    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4858    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4859
4860    try:
4861        if args.version:
4862            print("TKSBrokerAPI {}".format(buildVersion))
4863            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4864
4865        else:
4866            # Init class for trading with Tinkoff Broker:
4867            trader = TinkoffBrokerServer(
4868                token=args.token,
4869                accountId=args.account_id,
4870                useCache=not args.no_cache,
4871            )
4872
4873            # --- set some options:
4874
4875            if args.more:
4876                trader.moreDebug = True
4877                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4878
4879            if args.html:
4880                trader.useHTMLReports = True
4881
4882            if args.ticker:
4883                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4884
4885                if ticker in trader.aliasesKeys:
4886                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4887
4888                else:
4889                    trader.ticker = ticker
4890
4891            if args.figi:
4892                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4893
4894            if args.depth is not None:
4895                trader.depth = args.depth
4896
4897            # --- do one command:
4898
4899            if args.list:
4900                if args.output is not None:
4901                    trader.instrumentsFile = args.output
4902
4903                trader.ShowInstrumentsInfo(show=True)
4904
4905            elif args.list_xlsx:
4906                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4907
4908            elif args.bonds_xlsx is not None:
4909                if args.output is not None:
4910                    trader.bondsXLSXFile = args.output
4911
4912                if len(args.bonds_xlsx) == 0:
4913                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4914
4915                else:
4916                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4917
4918            elif args.search:
4919                if args.output is not None:
4920                    trader.searchResultsFile = args.output
4921
4922                trader.SearchInstruments(pattern=args.search[0], show=True)
4923
4924            elif args.info:
4925                if not (args.ticker or args.figi):
4926                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4927                    raise Exception("Ticker or FIGI required")
4928
4929                if args.output is not None:
4930                    trader.infoFile = args.output
4931
4932                if args.ticker:
4933                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4934
4935                else:
4936                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4937
4938            elif args.calendar is not None:
4939                if args.output is not None:
4940                    trader.calendarFile = args.output
4941
4942                if len(args.calendar) == 0:
4943                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4944
4945                else:
4946                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4947
4948                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4949
4950            elif args.price:
4951                if not (args.ticker or args.figi):
4952                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4953                    raise Exception("Ticker or FIGI required")
4954
4955                trader.GetCurrentPrices(show=True)
4956
4957            elif args.prices is not None:
4958                if args.output is not None:
4959                    trader.pricesFile = args.output
4960
4961                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4962
4963            elif args.overview:
4964                if args.output is not None:
4965                    trader.overviewFile = args.output
4966
4967                trader.Overview(show=True, details="full")
4968
4969            elif args.overview_digest:
4970                if args.output is not None:
4971                    trader.overviewDigestFile = args.output
4972
4973                trader.Overview(show=True, details="digest")
4974
4975            elif args.overview_positions:
4976                if args.output is not None:
4977                    trader.overviewPositionsFile = args.output
4978
4979                trader.Overview(show=True, details="positions")
4980
4981            elif args.overview_orders:
4982                if args.output is not None:
4983                    trader.overviewOrdersFile = args.output
4984
4985                trader.Overview(show=True, details="orders")
4986
4987            elif args.overview_analytics:
4988                if args.output is not None:
4989                    trader.overviewAnalyticsFile = args.output
4990
4991                trader.Overview(show=True, details="analytics")
4992
4993            elif args.overview_calendar:
4994                if args.output is not None:
4995                    trader.overviewAnalyticsFile = args.output
4996
4997                trader.Overview(show=True, details="calendar")
4998
4999            elif args.deals is not None:
5000                if args.output is not None:
5001                    trader.reportFile = args.output
5002
5003                if 0 <= len(args.deals) < 3:
5004                    trader.Deals(
5005                        start=args.deals[0] if len(args.deals) >= 1 else None,
5006                        end=args.deals[1] if len(args.deals) == 2 else None,
5007                        show=True,  # Always show deals report in console
5008                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
5009                    )
5010
5011                else:
5012                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5013                    raise Exception("Incorrect value")
5014
5015            elif args.history is not None:
5016                if args.output is not None:
5017                    trader.historyFile = args.output
5018
5019                if 0 <= len(args.history) < 3:
5020                    dataReceived = trader.History(
5021                        start=args.history[0] if len(args.history) >= 1 else None,
5022                        end=args.history[1] if len(args.history) == 2 else None,
5023                        interval="hour" if args.interval is None or not args.interval else args.interval,
5024                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
5025                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
5026                        show=True,  # shows all downloaded candles in console
5027                    )
5028
5029                    if args.render_chart is not None and dataReceived is not None:
5030                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5031
5032                        trader.ShowHistoryChart(
5033                            candles=dataReceived,
5034                            interact=iChart,
5035                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5036                        )
5037
5038                else:
5039                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5040                    raise Exception("Incorrect value")
5041
5042            elif args.load_history is not None:
5043                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5044
5045                if args.render_chart is not None and histData is not None:
5046                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5047                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5048
5049                    trader.ShowHistoryChart(
5050                        candles=histData,
5051                        interact=iChart,
5052                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5053                    )
5054
5055            elif args.trade is not None:
5056                if 1 <= len(args.trade) <= 5:
5057                    trader.Trade(
5058                        operation=args.trade[0],
5059                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5060                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5061                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5062                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5063                    )
5064
5065                else:
5066                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5067
5068            elif args.buy is not None:
5069                if 0 <= len(args.buy) <= 4:
5070                    trader.Buy(
5071                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5072                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5073                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5074                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5075                    )
5076
5077                else:
5078                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5079
5080            elif args.sell is not None:
5081                if 0 <= len(args.sell) <= 4:
5082                    trader.Sell(
5083                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5084                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5085                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5086                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5087                    )
5088
5089                else:
5090                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5091
5092            elif args.order:
5093                if 4 <= len(args.order) <= 7:
5094                    trader.Order(
5095                        operation=args.order[0],
5096                        orderType=args.order[1],
5097                        lots=int(args.order[2]),
5098                        targetPrice=float(args.order[3]),
5099                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5100                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5101                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5102                    )
5103
5104                else:
5105                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5106
5107            elif args.buy_limit:
5108                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5109
5110            elif args.sell_limit:
5111                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5112
5113            elif args.buy_stop:
5114                if 2 <= len(args.buy_stop) <= 7:
5115                    trader.BuyStop(
5116                        lots=int(args.buy_stop[0]),
5117                        targetPrice=float(args.buy_stop[1]),
5118                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5119                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5120                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5121                    )
5122
5123                else:
5124                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5125
5126            elif args.sell_stop:
5127                if 2 <= len(args.sell_stop) <= 7:
5128                    trader.SellStop(
5129                        lots=int(args.sell_stop[0]),
5130                        targetPrice=float(args.sell_stop[1]),
5131                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5132                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5133                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5134                    )
5135
5136                else:
5137                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5138
5139            # elif args.buy_order_grid is not None:
5140            #     # update order grid work with api v2
5141            #     if len(args.buy_order_grid) == 2:
5142            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5143            #
5144            #         for order in orderParams:
5145            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5146            #
5147            #     else:
5148            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5149            #
5150            # elif args.sell_order_grid is not None:
5151            #     # update order grid work with api v2
5152            #     if len(args.sell_order_grid) >= 2:
5153            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5154            #
5155            #         for order in orderParams:
5156            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5157            #
5158            #     else:
5159            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5160
5161            elif args.close_order is not None:
5162                trader.CloseOrders(args.close_order)  # close only one order
5163
5164            elif args.close_orders is not None:
5165                trader.CloseOrders(args.close_orders)  # close list of orders
5166
5167            elif args.close_trade:
5168                if not (args.ticker or args.figi):
5169                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5170                    raise Exception("Ticker or FIGI required")
5171
5172                if args.ticker:
5173                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5174
5175                else:
5176                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5177
5178            elif args.close_trades is not None:
5179                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5180
5181            elif args.close_all is not None:
5182                if args.ticker:
5183                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5184
5185                elif args.figi:
5186                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5187
5188                else:
5189                    trader.CloseAll(*args.close_all)
5190
5191            elif args.limits:
5192                if args.output is not None:
5193                    trader.withdrawalLimitsFile = args.output
5194
5195                trader.OverviewLimits(show=True)
5196
5197            elif args.user_info:
5198                if args.output is not None:
5199                    trader.userInfoFile = args.output
5200
5201                trader.OverviewUserInfo(show=True)
5202
5203            elif args.account:
5204                if args.output is not None:
5205                    trader.userAccountsFile = args.output
5206
5207                trader.OverviewAccounts(show=True)
5208
5209            else:
5210                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5211                raise Exception("There is no command to execute")
5212
5213    except Exception:
5214        trace = tb.format_exc()
5215        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5216            if e in trace:
5217                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5218                break
5219
5220        uLogger.debug(trace)
5221        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5222        exitCode = 255  # an error occurred, must be open a ticket for this issue
5223
5224    finally:
5225        finish = datetime.now(tzutc())
5226
5227        if exitCode == 0:
5228            if args.more:
5229                uLogger.debug("All operations were finished success (summary code is 0).")
5230
5231        else:
5232            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5233                os.path.abspath(uLog.defaultLogFile), exitCode,
5234            ))
5235
5236        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5237        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5238            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5239            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5240        ))
5241        uLogger.debug("=-" * 50)
5242
5243        if not kwargs:
5244            sys.exit(exitCode)
5245
5246        else:
5247            return exitCode
5248
5249
5250if __name__ == "__main__":
5251    Main()
class TinkoffBrokerServer:
  78class TinkoffBrokerServer:
  79    """
  80    This class implements methods to work with Tinkoff broker server.
  81
  82    Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
  83
  84    About `token`: https://tinkoff.github.io/investAPI/token/
  85    """
  86    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
  87        """
  88        Main class init.
  89
  90        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
  91        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
  92                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
  93        :param useCache: use default cache file with raw data to use instead of `iList`.
  94                         True by default. Cache is auto-update if new day has come.
  95                         If you don't want to use cache and always updates raw data then set `useCache=False`.
  96        :param defaultCache: path to default cache file. `dump.json` by default.
  97        """
  98        if token is None or not token:
  99            try:
 100                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
 101                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
 102
 103            except KeyError:
 104                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
 105                raise Exception("Token required")
 106
 107        else:
 108            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
 109            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
 110
 111        if accountId is None or not accountId:
 112            try:
 113                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
 114                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
 115
 116            except KeyError:
 117                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
 118
 119        else:
 120            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
 121            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
 122
 123        self.version = __version__  # duplicate here used TKSBrokerAPI main version
 124        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
 125
 126        Latest version: https://pypi.org/project/tksbrokerapi/
 127        """
 128
 129        self.__lock = Lock()  # initialize multiprocessing mutex lock
 130
 131        self.aliases = TKS_TICKER_ALIASES
 132        """Some aliases instead official tickers.
 133
 134        See also: `TKSEnums.TKS_TICKER_ALIASES`
 135        """
 136
 137        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
 138
 139        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
 140
 141        self._ticker = ""
 142        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 143
 144        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 145        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 146
 147        See also: `SearchByTicker()`, `SearchInstruments()`.
 148        """
 149
 150        self._figi = ""
 151        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 152
 153        See also: `SearchByFIGI()`, `SearchInstruments()`.
 154        """
 155
 156        self.depth = 1
 157        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
 158
 159        See also: `GetCurrentPrices()`.
 160        """
 161
 162        self.server = r"https://invest-public-api.tinkoff.ru/rest"
 163        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
 164
 165        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
 166        """
 167
 168        uLogger.debug("Broker API server: {}".format(self.server))
 169
 170        self.timeout = 15
 171        """Server operations timeout in seconds. Default: `15`.
 172
 173        See also: `SendAPIRequest()`.
 174        """
 175
 176        self.headers = {
 177            "Content-Type": "application/json",
 178            "accept": "application/json",
 179            "Authorization": "Bearer {}".format(self.token),
 180            "x-app-name": "Tim55667757.TKSBrokerAPI",
 181        }
 182        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
 183
 184        See also: `SendAPIRequest()`.
 185        """
 186
 187        self.body = None
 188        """Request body which send to broker server. Default: `None`.
 189
 190        See also: `SendAPIRequest()`.
 191        """
 192
 193        self.moreDebug = False
 194        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
 195
 196        self.useHTMLReports = False
 197        """
 198        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
 199        
 200        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
 201        """
 202
 203        self.historyFile = None
 204        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
 205
 206        See also: `History()`.
 207        """
 208
 209        self.htmlHistoryFile = "index.html"
 210        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
 211
 212        See also: `ShowHistoryChart()`.
 213        """
 214
 215        self.instrumentsFile = "instruments.md"
 216        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
 217
 218        See also: `ShowInstrumentsInfo()`.
 219        """
 220
 221        self.searchResultsFile = "search-results.md"
 222        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
 223
 224        See also: `SearchInstruments()`.
 225        """
 226
 227        self.pricesFile = "prices.md"
 228        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 229
 230        See also: `GetListOfPrices()`.
 231        """
 232
 233        self.infoFile = "info.md"
 234        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
 235
 236        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
 237        """
 238
 239        self.bondsXLSXFile = "ext-bonds.xlsx"
 240        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
 241        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
 242
 243        See also: `ExtendBondsData()`.
 244        """
 245
 246        self.calendarFile = "calendar.md"
 247        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
 248        
 249        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
 250
 251        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
 252        """
 253
 254        self.overviewFile = "overview.md"
 255        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
 256
 257        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
 258        """
 259
 260        self.overviewDigestFile = "overview-digest.md"
 261        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
 262
 263        See also: `Overview()` with parameter `details="digest"`.
 264        """
 265
 266        self.overviewPositionsFile = "overview-positions.md"
 267        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
 268
 269        See also: `Overview()` with parameter `details="positions"`.
 270        """
 271
 272        self.overviewOrdersFile = "overview-orders.md"
 273        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
 274
 275        See also: `Overview()` with parameter `details="orders"`.
 276        """
 277
 278        self.overviewAnalyticsFile = "overview-analytics.md"
 279        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
 280
 281        See also: `Overview()` with parameter `details="analytics"`.
 282        """
 283
 284        self.overviewBondsCalendarFile = "overview-calendar.md"
 285        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
 286
 287        See also: `Overview()` with parameter `details="calendar"`.
 288        """
 289
 290        self.reportFile = "deals.md"
 291        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
 292
 293        See also: `Deals()`.
 294        """
 295
 296        self.withdrawalLimitsFile = "limits.md"
 297        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
 298
 299        See also: `OverviewLimits()` and `RequestLimits()`.
 300        """
 301
 302        self.userInfoFile = "user-info.md"
 303        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
 304
 305        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
 306        """
 307
 308        self.userAccountsFile = "accounts.md"
 309        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
 310
 311        See also: `OverviewAccounts()`, `RequestAccounts()`.
 312        """
 313
 314        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
 315        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
 316
 317        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
 318
 319        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
 320        """
 321
 322        self.iList = None  # init iList for raw instruments data
 323        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
 324        
 325        See also: `Listing()`, `DumpInstruments()`.
 326        """
 327
 328        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
 329        if useCache:
 330            if os.path.exists(self.iListDumpFile):
 331                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
 332                curTime = datetime.now(tzutc())
 333
 334                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
 335                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
 336
 337                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
 338
 339                else:
 340                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
 341
 342                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
 343                        os.path.abspath(self.iListDumpFile),
 344                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
 345                    ))
 346
 347            else:
 348                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
 349                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
 350
 351        else:
 352            self.iList = self.Listing()  # request new raw instruments data from broker server
 353            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
 354
 355        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
 356        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
 357
 358        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
 359        """
 360
 361    @property
 362    def ticker(self) -> str:
 363        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 364
 365        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 366        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 367
 368        See also: `SearchByTicker()`, `SearchInstruments()`.
 369        """
 370        return self._ticker
 371
 372    @ticker.setter
 373    def ticker(self, value):
 374        """Setter for string with ticker, e.g. `GOOGL`. Tickers may be upper case only.
 375
 376        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
 377        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
 378
 379        See also: `SearchByTicker()`, `SearchInstruments()`.
 380        """
 381        self._ticker = str(value).upper()  # Tickers may be upper case only
 382
 383    @property
 384    def figi(self) -> str:
 385        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 386
 387        See also: `SearchByFIGI()`, `SearchInstruments()`.
 388        """
 389        return self._figi
 390
 391    @figi.setter
 392    def figi(self, value):
 393        """Setter for string with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
 394
 395        See also: `SearchByFIGI()`, `SearchInstruments()`.
 396        """
 397        self._figi = str(value).upper()  # FIGI may be upper case only
 398
 399    def _ParseJSON(self, rawData="{}") -> dict:
 400        """
 401        Parse JSON from response string.
 402
 403        :param rawData: this is a string with JSON-formatted text.
 404        :return: JSON (dictionary), parsed from server response string. If an error occurred, then returns empty dict `{}`.
 405        """
 406        try:
 407            responseJSON = json.loads(rawData) if rawData else {}
 408
 409            if self.moreDebug:
 410                uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4)))
 411
 412            return responseJSON
 413
 414        except Exception as e:
 415            uLogger.error("An empty dict will be return, because an error occurred in `_ParseJSON()` method with comment: {}".format(e))
 416
 417            return {}
 418
 419    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
 420        """
 421        Send GET or POST request to broker server and receive JSON object.
 422
 423        self.header: must be defining with dictionary of headers.
 424        self.body: if define then used as request body. None by default.
 425        self.timeout: global request timeout, 15 seconds by default.
 426        :param url: url with REST request.
 427        :param reqType: send "GET" or "POST" request. "GET" by default.
 428        :param retry: how many times retry after first request if an 5xx server errors occurred.
 429        :param pause: sleep time in seconds between retries.
 430        :return: response JSON (dictionary) from broker.
 431        """
 432        if reqType.upper() not in ("GET", "POST"):
 433            uLogger.error("You can define request type: `GET` or `POST`!")
 434            raise Exception("Incorrect value")
 435
 436        if self.moreDebug:
 437            uLogger.debug("Request parameters:")
 438            uLogger.debug("    - REST API URL: {}".format(url))
 439            uLogger.debug("    - request type: {}".format(reqType))
 440            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
 441            uLogger.debug("    - body:\n{}".format(self.body))
 442
 443        # fast hack to avoid all operations with some tickers/FIGI
 444        responseJSON = {}
 445        oK = True
 446        for item in self.exclude:
 447            if item in url:
 448                if self.moreDebug:
 449                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
 450
 451                oK = False
 452                break
 453
 454        if oK:
 455            with self.__lock:  # acquire the mutex lock
 456                counter = 0
 457                response = None
 458                errMsg = ""
 459
 460                while not response and counter <= retry:
 461                    if reqType == "GET":
 462                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
 463
 464                    if reqType == "POST":
 465                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
 466
 467                    if self.moreDebug:
 468                        uLogger.debug("Response:")
 469                        uLogger.debug("    - status code: {}".format(response.status_code))
 470                        uLogger.debug("    - reason: {}".format(response.reason))
 471                        uLogger.debug("    - body length: {}".format(len(response.text)))
 472                        uLogger.debug("    - headers:\n{}".format(response.headers))
 473
 474                    # Server returns some headers:
 475                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
 476                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
 477                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
 478                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
 479                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
 480                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
 481                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
 482                        sleep(rateLimitWait)
 483
 484                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
 485                    if 400 <= response.status_code < 500:
 486                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 487                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
 488
 489                        if "code" in response.text and "message" in response.text:
 490                            msgDict = self._ParseJSON(rawData=response.text)
 491                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
 492
 493                        counter = retry + 1  # do not retry for 4xx errors
 494
 495                    if 500 <= response.status_code < 600:
 496                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
 497                        uLogger.debug("    - not oK, {}".format(errMsg))
 498
 499                        if "code" in response.text and "message" in response.text:
 500                            errMsgDict = self._ParseJSON(rawData=response.text)
 501                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
 502
 503                        counter += 1
 504
 505                        if counter <= retry:
 506                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
 507                            sleep(pause)
 508
 509                responseJSON = self._ParseJSON(rawData=response.text)
 510
 511                if errMsg:
 512                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
 513                    uLogger.error("    - not oK, {}".format(errMsg))
 514
 515        return responseJSON
 516
 517    def _IUpdater(self, iType: str) -> tuple:
 518        """
 519        Request instrument by type from server. See available API methods for instruments:
 520        Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies
 521        Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares
 522        Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds
 523        Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs
 524        Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures
 525
 526        :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 527        :return: tuple with iType name and list of available instruments of current type for defined user token.
 528        """
 529        result = []
 530
 531        if iType in TKS_INSTRUMENTS:
 532            uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType))
 533
 534            # all instruments have the same body in API v2 requests:
 535            self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"})  # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL]
 536            instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType)
 537            result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"]
 538
 539        return iType, result
 540
 541    def _IWrapper(self, kwargs):
 542        """
 543        Wrapper runs instrument's update method `_IUpdater()`.
 544        It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206
 545        """
 546        return self._IUpdater(**kwargs)
 547
 548    def Listing(self) -> dict:
 549        """
 550        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
 551
 552        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
 553        """
 554        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
 555        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
 556
 557        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
 558        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
 559        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
 560
 561        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
 562        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
 563        poolUpdater.close()  # close the thread pool
 564        poolUpdater.join()  # wait a moment until all data returns from threads
 565
 566        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
 567        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
 568        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
 569
 570        # calculate minimum price increment (step) for all instruments and set up instrument's type:
 571        for iType in iList.keys():
 572            for ticker in iList[iType]:
 573                iList[iType][ticker]["type"] = iType
 574
 575                if "minPriceIncrement" in iList[iType][ticker].keys():
 576                    iList[iType][ticker]["step"] = NanoToFloat(
 577                        iList[iType][ticker]["minPriceIncrement"]["units"],
 578                        iList[iType][ticker]["minPriceIncrement"]["nano"],
 579                    )
 580
 581                else:
 582                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
 583
 584        return iList
 585
 586    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
 587        """
 588        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
 589
 590        See also: `DumpInstruments()`, `Listing()`.
 591
 592        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 593                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
 594        """
 595        if self.iListDumpFile is None or not self.iListDumpFile:
 596            uLogger.error("Output name of dump file must be defined!")
 597            raise Exception("Filename required")
 598
 599        if not self.iList or forceUpdate:
 600            self.iList = self.Listing()
 601
 602        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
 603
 604        # Save as XLSX with separated sheets for every type of instruments:
 605        with pd.ExcelWriter(
 606                path=xlsxDumpFile,
 607                date_format=TKS_DATE_FORMAT,
 608                datetime_format=TKS_DATE_TIME_FORMAT,
 609                mode="w",
 610        ) as writer:
 611            for iType in TKS_INSTRUMENTS:
 612                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
 613                df = df[sorted(df)]  # sorted by column names
 614                df = df.applymap(
 615                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
 616                    na_action="ignore",
 617                )  # converting numbers from nano-type to float in every cell
 618                df.to_excel(
 619                    writer,
 620                    sheet_name=iType,
 621                    encoding="UTF-8",
 622                    freeze_panes=(1, 1),
 623                )  # saving as XLSX-file with freeze first row and column as headers
 624
 625        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
 626
 627    def DumpInstruments(self, forceUpdate: bool = True) -> str:
 628        """
 629        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
 630        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
 631
 632        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
 633
 634        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
 635                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
 636        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
 637        """
 638        if self.iListDumpFile is None or not self.iListDumpFile:
 639            uLogger.error("Output name of dump file must be defined!")
 640            raise Exception("Filename required")
 641
 642        if not self.iList or forceUpdate:
 643            self.iList = self.Listing()
 644
 645        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
 646        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
 647            fH.write(jsonDump)
 648
 649        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
 650
 651        return jsonDump
 652
 653    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
 654        """
 655        Show information about one instrument defined by json data and prints it in Markdown format.
 656
 657        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
 658
 659        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
 660        :param show: if `True` then also printing information about instrument and its current price.
 661        :return: multilines text in Markdown format with information about one instrument.
 662        """
 663        splitLine = "|                                                             |                                                        |\n"
 664        infoText = ""
 665
 666        if iJSON is not None and iJSON and isinstance(iJSON, dict):
 667            info = [
 668                "# Main information\n\n",
 669                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
 670                "| Parameters                                                  | Values                                                 |\n",
 671                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
 672                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
 673                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
 674            ]
 675
 676            if "sector" in iJSON.keys() and iJSON["sector"]:
 677                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
 678
 679            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
 680                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
 681
 682            info.extend([
 683                splitLine,
 684                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
 685                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
 686            ])
 687
 688            if "isin" in iJSON.keys() and iJSON["isin"]:
 689                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
 690
 691            if "classCode" in iJSON.keys():
 692                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
 693
 694            info.extend([
 695                splitLine,
 696                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
 697                splitLine,
 698                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
 699                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
 700                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
 701            ])
 702
 703            if iJSON["figi"]:
 704                self._figi = iJSON["figi"]
 705                iJSON = iJSON | self.RequestTradingStatus()
 706
 707                info.extend([
 708                    splitLine,
 709                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
 710                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
 711                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
 712                ])
 713
 714            info.append(splitLine)
 715
 716            if "type" in iJSON.keys() and iJSON["type"]:
 717                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
 718
 719                if "shareType" in iJSON.keys() and iJSON["shareType"]:
 720                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
 721
 722            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
 723                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
 724
 725            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
 726                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
 727
 728            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
 729                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
 730
 731            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
 732                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
 733
 734            if "focusType" in iJSON.keys() and iJSON["focusType"]:
 735                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
 736
 737            if "assetType" in iJSON.keys() and iJSON["assetType"]:
 738                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
 739
 740            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
 741                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
 742
 743            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
 744                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
 745
 746            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
 747                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
 748
 749            if "currency" in iJSON.keys():
 750                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
 751
 752            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
 753                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
 754
 755            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
 756                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
 757
 758            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
 759                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
 760
 761            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
 762                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
 763
 764            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
 765                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
 766
 767            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
 768                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
 769
 770            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
 771                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
 772
 773            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
 774                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
 775
 776            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
 777                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
 778
 779            iExt = None
 780            if iJSON["type"] == "Bonds":
 781                info.extend([
 782                    splitLine,
 783                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
 784                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
 785                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
 786                        iJSON["nominal"]["currency"],
 787                    )),
 788                ])
 789
 790                if "floatingCouponFlag" in iJSON.keys():
 791                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
 792
 793                if "amortizationFlag" in iJSON.keys():
 794                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
 795
 796                info.append(splitLine)
 797
 798                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
 799                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
 800
 801                if iJSON["figi"]:
 802                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
 803
 804                    info.extend([
 805                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
 806                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
 807                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
 808                    ])
 809
 810                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
 811                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
 812                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
 813                        iJSON["aciValue"]["currency"]
 814                    )))
 815
 816            if "currentPrice" in iJSON.keys():
 817                info.append(splitLine)
 818
 819                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
 820                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
 821
 822                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
 823                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
 824                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
 825                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
 826                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
 827
 828                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
 829                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
 830
 831                info.extend([
 832                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
 833                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
 834                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 835                    )),
 836                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
 837                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
 838                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
 839                    )),
 840                    "| Changes between last deal price and last close              | {:<54} |\n".format(
 841                        "{:.2f}%{}".format(
 842                            iJSON["currentPrice"]["changes"],
 843                            " ({}{:.2f} {})".format(
 844                                "+" if bondChangesDelta > 0 else "",
 845                                bondChangesDelta,
 846                                aciCurrency
 847                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
 848                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
 849                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
 850                                currency
 851                            ),
 852                        )
 853                    ),
 854                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
 855                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
 856                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 857                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
 858                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 859                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
 860                    )),
 861                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
 862                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
 863                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
 864                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
 865                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
 866                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
 867                    )),
 868                ])
 869
 870            if "lot" in iJSON.keys():
 871                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
 872
 873            if "step" in iJSON.keys() and iJSON["step"] != 0:
 874                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
 875
 876            # Add bond payment calendar:
 877            if iJSON["type"] == "Bonds":
 878                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
 879                info.extend(["\n#", strCalendar])
 880
 881            infoText += "".join(info)
 882
 883            if show:
 884                uLogger.info("{}".format(infoText))
 885
 886            else:
 887                uLogger.debug("{}".format(infoText))
 888
 889            if self.infoFile is not None:
 890                with open(self.infoFile, "w", encoding="UTF-8") as fH:
 891                    fH.write(infoText)
 892
 893                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
 894
 895                if self.useHTMLReports:
 896                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
 897                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
 898                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
 899
 900                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
 901
 902        return infoText
 903
 904    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
 905        """
 906        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
 907
 908        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
 909        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 910        :return: JSON formatted data with information about instrument.
 911        """
 912        tickerJSON = {}
 913        if self.moreDebug:
 914            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
 915
 916        if not self._ticker:
 917            uLogger.warning("self._ticker variable is not be empty!")
 918
 919        else:
 920            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
 921                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
 922                raise Exception("Instrument not allowed")
 923
 924            if not self.iList:
 925                self.iList = self.Listing()
 926
 927            if self._ticker in self.iList["Shares"].keys():
 928                tickerJSON = self.iList["Shares"][self._ticker]
 929                if self.moreDebug:
 930                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
 931
 932            elif self._ticker in self.iList["Currencies"].keys():
 933                tickerJSON = self.iList["Currencies"][self._ticker]
 934                if self.moreDebug:
 935                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
 936
 937            elif self._ticker in self.iList["Bonds"].keys():
 938                tickerJSON = self.iList["Bonds"][self._ticker]
 939                if self.moreDebug:
 940                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
 941
 942            elif self._ticker in self.iList["Etfs"].keys():
 943                tickerJSON = self.iList["Etfs"][self._ticker]
 944                if self.moreDebug:
 945                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
 946
 947            elif self._ticker in self.iList["Futures"].keys():
 948                tickerJSON = self.iList["Futures"][self._ticker]
 949                if self.moreDebug:
 950                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
 951
 952        if tickerJSON:
 953            self._figi = tickerJSON["figi"]
 954
 955            if requestPrice:
 956                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
 957
 958                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
 959                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
 960
 961                else:
 962                    tickerJSON["currentPrice"]["changes"] = 0
 963
 964            if show:
 965                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
 966
 967        else:
 968            if show:
 969                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
 970
 971        return tickerJSON
 972
 973    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 974        """
 975        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 976
 977        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 978        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 979        :return: JSON formatted data with information about instrument.
 980        """
 981        figiJSON = {}
 982        if self.moreDebug:
 983            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 984
 985        if not self._figi:
 986            uLogger.warning("self._figi variable is not be empty!")
 987
 988        else:
 989            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 990                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 991                raise Exception("Instrument not allowed")
 992
 993            if not self.iList:
 994                self.iList = self.Listing()
 995
 996            for item in self.iList["Shares"].keys():
 997                if self._figi == self.iList["Shares"][item]["figi"]:
 998                    figiJSON = self.iList["Shares"][item]
 999
1000                    if self.moreDebug:
1001                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
1002
1003                    break
1004
1005            if not figiJSON:
1006                for item in self.iList["Currencies"].keys():
1007                    if self._figi == self.iList["Currencies"][item]["figi"]:
1008                        figiJSON = self.iList["Currencies"][item]
1009
1010                        if self.moreDebug:
1011                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1012
1013                        break
1014
1015            if not figiJSON:
1016                for item in self.iList["Bonds"].keys():
1017                    if self._figi == self.iList["Bonds"][item]["figi"]:
1018                        figiJSON = self.iList["Bonds"][item]
1019
1020                        if self.moreDebug:
1021                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1022
1023                        break
1024
1025            if not figiJSON:
1026                for item in self.iList["Etfs"].keys():
1027                    if self._figi == self.iList["Etfs"][item]["figi"]:
1028                        figiJSON = self.iList["Etfs"][item]
1029
1030                        if self.moreDebug:
1031                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1032
1033                        break
1034
1035            if not figiJSON:
1036                for item in self.iList["Futures"].keys():
1037                    if self._figi == self.iList["Futures"][item]["figi"]:
1038                        figiJSON = self.iList["Futures"][item]
1039
1040                        if self.moreDebug:
1041                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1042
1043                        break
1044
1045        if figiJSON:
1046            self._figi = figiJSON["figi"]
1047            self._ticker = figiJSON["ticker"]
1048
1049            if requestPrice:
1050                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1051
1052                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1053                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1054
1055                else:
1056                    figiJSON["currentPrice"]["changes"] = 0
1057
1058            if show:
1059                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1060
1061        else:
1062            if show:
1063                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1064
1065        return figiJSON
1066
1067    def GetCurrentPrices(self, show: bool = True) -> dict:
1068        """
1069        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1070        `{"buy": [{"price": 1243.8, "quantity": 193},
1071                  {"price": 1244.0, "quantity": 168},
1072                  {"price": 1244.8, "quantity": 5},
1073                  {"price": 1245.0, "quantity": 61},
1074                  {"price": 1245.4, "quantity": 60}],
1075          "sell": [{"price": 1243.6, "quantity": 8},
1076                   {"price": 1242.6, "quantity": 10},
1077                   {"price": 1242.4, "quantity": 18},
1078                   {"price": 1242.2, "quantity": 50},
1079                   {"price": 1242.0, "quantity": 113}],
1080          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1081        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1082        - sell: list of dicts with Buyers prices,
1083            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1084            - quantity: volume value by current price in lots,
1085        - limitUp: current trade session limit price, maximum,
1086        - limitDown: current trade session limit price, minimum,
1087        - lastPrice: last deal price of the instrument,
1088        - closePrice: previous trade session close price of the instrument.
1089
1090        See also: `SearchByTicker()` and `SearchByFIGI()`.
1091        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1092        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1093
1094        :param show: if `True` then print DOM to log and console.
1095        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1096                 If an error occurred then returns an empty record:
1097                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1098        """
1099        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1100
1101        if self.depth < 1:
1102            uLogger.error("Depth of Market (DOM) must be >=1!")
1103            raise Exception("Incorrect value")
1104
1105        if not (self._ticker or self._figi):
1106            uLogger.error("self._ticker or self._figi variables must be defined!")
1107            raise Exception("Ticker or FIGI required")
1108
1109        if self._ticker and not self._figi:
1110            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1111            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1112
1113        if not self._ticker and self._figi:
1114            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1115            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1116
1117        if not self._figi:
1118            uLogger.error("FIGI is not defined!")
1119            raise Exception("Ticker or FIGI required")
1120
1121        else:
1122            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1123
1124            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1125            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1126            self.body = str({"figi": self._figi, "depth": self.depth})
1127            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1128
1129            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1130                # list of dicts with sellers orders:
1131                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1132
1133                # list of dicts with buyers orders:
1134                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1135
1136                # max price of instrument at this time:
1137                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1138
1139                # min price of instrument at this time:
1140                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1141
1142                # last price of deal with instrument:
1143                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1144
1145                # last close price of instrument:
1146                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1147
1148            else:
1149                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1150                uLogger.debug("Server response: {}".format(pricesResponse))
1151
1152            if show:
1153                if prices["buy"] or prices["sell"]:
1154                    info = [
1155                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1156                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1157                            self._ticker,
1158                            self._figi,
1159                            self.depth,
1160                        ),
1161                        "-" * 60, "\n",
1162                        "             Orders of Buyers | Orders of Sellers\n",
1163                        "-" * 60, "\n",
1164                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1165                        "-" * 60, "\n",
1166                    ]
1167
1168                    if not prices["buy"]:
1169                        info.append("                              | No orders!\n")
1170                        sumBuy = 0
1171
1172                    else:
1173                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1174                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1175                        for item in maxMinSorted:
1176                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1177
1178                    if not prices["sell"]:
1179                        info.append("No orders!                    |\n")
1180                        sumSell = 0
1181
1182                    else:
1183                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1184                        for item in prices["sell"]:
1185                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1186
1187                    info.extend([
1188                        "-" * 60, "\n",
1189                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1190                        "-" * 60, "\n",
1191                    ])
1192
1193                    infoText = "".join(info)
1194
1195                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1196
1197                else:
1198                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1199
1200        return prices
1201
1202    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1203        """
1204        This method get and show information about all available broker instruments for current user account.
1205        If `instrumentsFile` string is not empty then also save information to this file.
1206
1207        :param show: if `True` then print results to console, if `False` — print only to file.
1208        :return: multi-lines string with all available broker instruments
1209        """
1210        if not self.iList:
1211            self.iList = self.Listing()
1212
1213        info = [
1214            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1215            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1216        ]
1217
1218        # add instruments count by type:
1219        for iType in self.iList.keys():
1220            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1221
1222        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1223        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1224
1225        # generating info tables with all instruments by type:
1226        for iType in self.iList.keys():
1227            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1228
1229            for instrument in self.iList[iType].keys():
1230                iName = self.iList[iType][instrument]["name"]  # instrument's name
1231                if len(iName) > 57:
1232                    iName = "{}...".format(iName[:54])  # right trim for a long string
1233
1234                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1235                    self.iList[iType][instrument]["ticker"],
1236                    iName,
1237                    self.iList[iType][instrument]["figi"],
1238                    self.iList[iType][instrument]["currency"],
1239                    self.iList[iType][instrument]["lot"],
1240                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1241                ))
1242
1243        infoText = "".join(info)
1244
1245        if show:
1246            uLogger.info(infoText)
1247
1248        if self.instrumentsFile:
1249            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1250                fH.write(infoText)
1251
1252            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1253
1254            if self.useHTMLReports:
1255                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1256                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1257                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1258
1259                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1260
1261        return infoText
1262
1263    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1264        """
1265        This method search and show information about instruments by part of its ticker, FIGI or name.
1266        If `searchResultsFile` string is not empty then also save information to this file.
1267
1268        :param pattern: string with part of ticker, FIGI or instrument's name.
1269        :param show: if `True` then print results to console, if `False` — return list of result only.
1270        :return: list of dictionaries with all found instruments.
1271        """
1272        if not self.iList:
1273            self.iList = self.Listing()
1274
1275        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1276        compiledPattern = re.compile(pattern, re.IGNORECASE)
1277
1278        for iType in self.iList:
1279            for instrument in self.iList[iType].values():
1280                searchResult = compiledPattern.search(" ".join(
1281                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1282                ))
1283
1284                if searchResult:
1285                    searchResults[iType][instrument["ticker"]] = instrument
1286
1287        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1288        info = [
1289            "# Search results\n\n",
1290            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1291            "* **Search pattern:** [{}]\n".format(pattern),
1292            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1293            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1294        ]
1295        infoShort = info[:]
1296
1297        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1298        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1299        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1300
1301        if resultsLen == 0:
1302            info.append("\nNo results\n")
1303            infoShort.append("\nNo results\n")
1304            uLogger.warning("No results. Try changing your search pattern.")
1305
1306        else:
1307            for iType in searchResults:
1308                iTypeValuesCount = len(searchResults[iType].values())
1309                if iTypeValuesCount > 0:
1310                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1311                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1312
1313                    for instrument in searchResults[iType].values():
1314                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1315                            instrument["type"],
1316                            instrument["ticker"],
1317                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1318                            instrument["figi"],
1319                        ))
1320
1321                    if iTypeValuesCount <= 5:
1322                        infoShort.extend(info[-iTypeValuesCount:])
1323
1324                    else:
1325                        infoShort.extend(info[-5:])
1326                        infoShort.append(skippedLine)
1327
1328        infoText = "".join(info)
1329        infoTextShort = "".join(infoShort)
1330
1331        if show:
1332            uLogger.info(infoTextShort)
1333            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1334
1335        if self.searchResultsFile:
1336            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1337                fH.write(infoText)
1338
1339            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1340
1341            if self.useHTMLReports:
1342                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1343                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1344                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1345
1346                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1347
1348        return searchResults
1349
1350    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1351        """
1352        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1353
1354        :param instruments: list of strings with tickers or FIGIs.
1355        :return: list with unique instrument FIGIs only.
1356        """
1357        requestedInstruments = []
1358        for iName in instruments:
1359            if iName not in self.aliases.keys():
1360                if iName not in requestedInstruments:
1361                    requestedInstruments.append(iName)
1362
1363            else:
1364                if iName not in requestedInstruments:
1365                    if self.aliases[iName] not in requestedInstruments:
1366                        requestedInstruments.append(self.aliases[iName])
1367
1368        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1369
1370        onlyUniqueFIGIs = []
1371        for iName in requestedInstruments:
1372            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1373                continue
1374
1375            self._ticker = iName
1376            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1377
1378            if not iData:
1379                self._ticker = ""
1380                self._figi = iName
1381
1382                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1383
1384                if not iData:
1385                    self._figi = ""
1386                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1387
1388            if iData and iData["figi"] not in onlyUniqueFIGIs:
1389                onlyUniqueFIGIs.append(iData["figi"])
1390
1391        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1392
1393        return onlyUniqueFIGIs
1394
1395    def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1396        """
1397        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1398
1399        See limits: https://tinkoff.github.io/investAPI/limits/
1400
1401        If `pricesFile` string is not empty then also save information to this file.
1402
1403        :param instruments: list of strings with tickers or FIGIs.
1404        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1405        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1406                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1407        """
1408        if instruments is None or not instruments:
1409            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1410            raise Exception("Ticker or FIGI required")
1411
1412        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1413
1414        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1415
1416        iList = []  # trying to get info and current prices about all unique instruments:
1417        for self._figi in onlyUniqueFIGIs:
1418            iData = self.SearchByFIGI(requestPrice=True)
1419            iList.append(iData)
1420
1421        self.ShowListOfPrices(iList, show)
1422
1423        return iList
1424
1425    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1426        """
1427        Show table contains current prices of given instruments.
1428
1429        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1430                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1431        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1432        :return: multilines text in Markdown format as a table contains current prices.
1433        """
1434        infoText = ""
1435
1436        if show or self.pricesFile:
1437            info = [
1438                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1439                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1440                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1441            ]
1442
1443            for item in iList:
1444                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1445                    item["ticker"],
1446                    item["figi"],
1447                    item["type"],
1448                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1449                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1450                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1451                    "{} / {}".format(
1452                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1453                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1454                    ),
1455                    "{} / {}".format(
1456                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1457                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1458                    ),
1459                    item["currency"],
1460                ))
1461
1462            infoText = "".join(info)
1463
1464            if show:
1465                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1466
1467            if self.pricesFile:
1468                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1469                    fH.write(infoText)
1470
1471                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1472
1473                if self.useHTMLReports:
1474                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1475                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1476                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1477
1478                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1479
1480        return infoText
1481
1482    def RequestTradingStatus(self) -> dict:
1483        """
1484        Requesting trading status for the instrument defined by `figi` variable.
1485
1486        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1487
1488        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1489
1490        :return: dictionary with trading status attributes. Response example:
1491                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1492                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1493        """
1494        if self._figi is None or not self._figi:
1495            uLogger.error("Variable `figi` must be defined for using this method!")
1496            raise Exception("FIGI required")
1497
1498        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1499
1500        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1501        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1502        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1503
1504        if self.moreDebug:
1505            uLogger.debug("Records about current trading status successfully received")
1506
1507        return tradingStatus
1508
1509    def RequestPortfolio(self) -> dict:
1510        """
1511        Requesting actual user's portfolio for current `accountId`.
1512
1513        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1514
1515        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1516
1517        :return: dictionary with user's portfolio.
1518        """
1519        if self.accountId is None or not self.accountId:
1520            uLogger.error("Variable `accountId` must be defined for using this method!")
1521            raise Exception("Account ID required")
1522
1523        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1524
1525        self.body = str({"accountId": self.accountId})
1526        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1527        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1528
1529        if self.moreDebug:
1530            uLogger.debug("Records about user's portfolio successfully received")
1531
1532        return rawPortfolio
1533
1534    def RequestPositions(self) -> dict:
1535        """
1536        Requesting open positions by currencies and instruments for current `accountId`.
1537
1538        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1539
1540        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1541
1542        :return: dictionary with open positions by instruments.
1543        """
1544        if self.accountId is None or not self.accountId:
1545            uLogger.error("Variable `accountId` must be defined for using this method!")
1546            raise Exception("Account ID required")
1547
1548        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1549
1550        self.body = str({"accountId": self.accountId})
1551        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1552        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1553
1554        if self.moreDebug:
1555            uLogger.debug("Records about current open positions successfully received")
1556
1557        return rawPositions
1558
1559    def RequestPendingOrders(self) -> list:
1560        """
1561        Requesting current actual pending limit orders for current `accountId`.
1562
1563        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1564
1565        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1566
1567        :return: list of dictionaries with pending limit orders.
1568        """
1569        if self.accountId is None or not self.accountId:
1570            uLogger.error("Variable `accountId` must be defined for using this method!")
1571            raise Exception("Account ID required")
1572
1573        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1574
1575        self.body = str({"accountId": self.accountId})
1576        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1577        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1578
1579        if "orders" in rawResponse.keys():
1580            rawOrders = rawResponse["orders"]
1581            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1582
1583        else:
1584            rawOrders = []
1585            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1586
1587        return rawOrders
1588
1589    def RequestStopOrders(self) -> list:
1590        """
1591        Requesting current actual stop orders for current `accountId`.
1592
1593        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1594
1595        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1596
1597        :return: list of dictionaries with stop orders.
1598        """
1599        if self.accountId is None or not self.accountId:
1600            uLogger.error("Variable `accountId` must be defined for using this method!")
1601            raise Exception("Account ID required")
1602
1603        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1604
1605        self.body = str({"accountId": self.accountId})
1606        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1607        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1608
1609        if "stopOrders" in rawResponse.keys():
1610            rawStopOrders = rawResponse["stopOrders"]
1611            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1612
1613        else:
1614            rawStopOrders = []
1615            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1616
1617        return rawStopOrders
1618
1619    def Overview(self, show: bool = False, details: str = "full") -> dict:
1620        """
1621        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1622        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1623        and `overviewBondsCalendarFile` are defined then also save information to file.
1624
1625        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1626        many requests about the state of the portfolio, and then, based on the received data, a large number
1627        of calculation and statistics are collected.
1628
1629        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1630        :param details: how detailed should the information be?
1631        - `full` — shows full available information about portfolio status (by default),
1632        - `positions` — shows only open positions,
1633        - `orders` — shows only sections of open limits and stop orders.
1634        - `digest` — show a short digest of the portfolio status,
1635        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1636        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1637        :return: dictionary with client's raw portfolio and some statistics.
1638        """
1639        if self.accountId is None or not self.accountId:
1640            uLogger.error("Variable `accountId` must be defined for using this method!")
1641            raise Exception("Account ID required")
1642
1643        view = {
1644            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1645                "headers": {},  # list of dictionaries, response headers without "positions" section
1646                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1647                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1648                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1649                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1650                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1651                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1652                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1653                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1654                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1655            },
1656            "stat": {  # --- some statistics calculated using "raw" sections:
1657                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1658                "availableRUB": 0.,  # available rubles (without other currencies)
1659                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1660                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1661                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1662                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1663                "sharesCostRUB": 0.,  # costs of all shares in RUB
1664                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1665                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1666                "futuresCostRUB": 0.,  # costs of all futures in RUB
1667                "Currencies": [],  # list of dictionaries of all currencies statistics
1668                "Shares": [],  # list of dictionaries of all shares statistics
1669                "Bonds": [],  # list of dictionaries of all bonds statistics
1670                "Etfs": [],  # list of dictionaries of all etfs statistics
1671                "Futures": [],  # list of dictionaries of all futures statistics
1672                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1673                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1674                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1675                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1676                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1677            },
1678            "analytics": {  # --- some analytics of portfolio:
1679                "distrByAssets": {},  # portfolio distribution by assets
1680                "distrByCompanies": {},  # portfolio distribution by companies
1681                "distrBySectors": {},  # portfolio distribution by sectors
1682                "distrByCurrencies": {},  # portfolio distribution by currencies
1683                "distrByCountries": {},  # portfolio distribution by countries
1684                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1685            }
1686        }
1687
1688        details = details.lower()
1689        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1690        if details not in availableDetails:
1691            details = "full"
1692            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1693
1694        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1695
1696        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1697        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1698        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1699        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1700
1701        # save response headers without "positions" section:
1702        for key in portfolioResponse.keys():
1703            if key != "positions":
1704                view["raw"]["headers"][key] = portfolioResponse[key]
1705
1706            else:
1707                continue
1708
1709        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1710        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1711        for item in portfolioResponse["positions"]:
1712            if item["instrumentType"] == "currency":
1713                self._figi = item["figi"]
1714                if not self._figi and item["ticker"]:
1715                    self._ticker = item["ticker"]
1716                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1717
1718                curr = self.SearchByFIGI(requestPrice=False)
1719
1720                # current price of currency in RUB:
1721                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1722                    "name": curr["name"],
1723                    "currentPrice": NanoToFloat(
1724                        item["currentPrice"]["units"],
1725                        item["currentPrice"]["nano"]
1726                    ),
1727                }
1728
1729                view["raw"]["Currencies"].append(item)
1730
1731            elif item["instrumentType"] == "share":
1732                view["raw"]["Shares"].append(item)
1733
1734            elif item["instrumentType"] == "bond":
1735                view["raw"]["Bonds"].append(item)
1736
1737            elif item["instrumentType"] == "etf":
1738                view["raw"]["Etfs"].append(item)
1739
1740            elif item["instrumentType"] == "futures":
1741                view["raw"]["Futures"].append(item)
1742
1743            else:
1744                continue
1745
1746        # how many volume of currencies (by ISO currency name) are blocked:
1747        for item in view["raw"]["positions"]["blocked"]:
1748            blocked = NanoToFloat(item["units"], item["nano"])
1749            if blocked > 0:
1750                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1751
1752        # how many volume of instruments (by FIGI) are blocked:
1753        for item in view["raw"]["positions"]["securities"]:
1754            blocked = int(item["blocked"])
1755            if blocked > 0:
1756                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1757
1758        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1759
1760        if "rub" in allBlocked.keys():
1761            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1762
1763        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1764        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1765        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1766        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1767        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1768        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1769        view["stat"]["portfolioCostRUB"] = sum([
1770            view["stat"]["allCurrenciesCostRUB"],
1771            view["stat"]["sharesCostRUB"],
1772            view["stat"]["bondsCostRUB"],
1773            view["stat"]["etfsCostRUB"],
1774            view["stat"]["futuresCostRUB"],
1775        ])
1776
1777        # --- calculating some portfolio statistics:
1778        byComp = {}  # distribution by companies
1779        bySect = {}  # distribution by sectors
1780        byCurr = {}  # distribution by currencies (include RUB)
1781        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1782        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1783
1784        for item in portfolioResponse["positions"]:
1785            self._figi = item["figi"]
1786            if not self._figi and item["ticker"]:
1787                self._ticker = item["ticker"]
1788                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1789
1790            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1791
1792            if instrument:
1793                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1794                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1795
1796                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1797                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1798
1799                else:
1800                    blocked = 0
1801
1802                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1803                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1804                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1805                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1806                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1807                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1808                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1809                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1810                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1811                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1812                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1813                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1814
1815                statData = {
1816                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1817                    "ticker": instrument["ticker"],  # ticker by FIGI
1818                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1819                    "volume": volume,  # available volume of instrument
1820                    "lots": lots,  # volume in lots of instrument
1821                    "direction": direction,  # direction of an instrument's position: short or long
1822                    "blocked": blocked,  # blocked volume of currency or instrument
1823                    "currentPrice": curPrice,  # current instrument's price in basic asset
1824                    "average": average,  # current average position price
1825                    "cost": cost,  # current cost of all volume of instrument in basic asset
1826                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1827                    "costRUB": costRUB,  # cost of instrument in ruble
1828                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1829                    "profit": profit,  # expected profit at current moment
1830                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1831                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1832                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1833                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1834                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1835                    "step": instrument["step"],  # minimum price increment
1836                }
1837
1838                # adding distribution by unique countries:
1839                if statData["country"] not in byCountry.keys():
1840                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1841
1842                else:
1843                    byCountry[statData["country"]]["cost"] += costRUB
1844                    byCountry[statData["country"]]["percent"] += percentCostRUB
1845
1846                if item["instrumentType"] != "currency":
1847                    # adding distribution by unique companies:
1848                    if statData["name"]:
1849                        if statData["name"] not in byComp.keys():
1850                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1851
1852                        else:
1853                            byComp[statData["name"]]["cost"] += costRUB
1854                            byComp[statData["name"]]["percent"] += percentCostRUB
1855
1856                    # adding distribution by unique sectors:
1857                    if statData["sector"] not in bySect.keys():
1858                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1859
1860                    else:
1861                        bySect[statData["sector"]]["cost"] += costRUB
1862                        bySect[statData["sector"]]["percent"] += percentCostRUB
1863
1864                # adding distribution by unique currencies:
1865                if currency not in byCurr.keys():
1866                    byCurr[currency] = {
1867                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1868                        "cost": costRUB,
1869                        "percent": percentCostRUB
1870                    }
1871
1872                else:
1873                    byCurr[currency]["cost"] += costRUB
1874                    byCurr[currency]["percent"] += percentCostRUB
1875
1876                # saving statistics for every instrument:
1877                if item["instrumentType"] == "currency":
1878                    view["stat"]["Currencies"].append(statData)
1879
1880                    # update dict with free funds for trading (total - blocked) by currencies
1881                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1882                    view["stat"]["funds"][currency] = {
1883                        "total": volume,
1884                        "totalCostRUB": costRUB,  # total volume cost in rubles
1885                        "free": volume - blocked,
1886                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1887                    }
1888
1889                elif item["instrumentType"] == "share":
1890                    view["stat"]["Shares"].append(statData)
1891
1892                elif item["instrumentType"] == "bond":
1893                    view["stat"]["Bonds"].append(statData)
1894
1895                elif item["instrumentType"] == "etf":
1896                    view["stat"]["Etfs"].append(statData)
1897
1898                elif item["instrumentType"] == "Futures":
1899                    view["stat"]["Futures"].append(statData)
1900
1901                else:
1902                    continue
1903
1904        # total changes in Russian Ruble:
1905        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1906        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1907        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1908        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1909        view["stat"]["funds"]["rub"] = {
1910            "total": view["stat"]["availableRUB"],
1911            "totalCostRUB": view["stat"]["availableRUB"],
1912            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1913            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1914        }
1915
1916        # --- pending limit orders sector data:
1917        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1918        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1919
1920        for item in view["raw"]["orders"]:
1921            self._figi = item["figi"]
1922
1923            if item["figi"] not in uniquePendingOrdersFIGIs:
1924                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1925
1926                uniquePendingOrdersFIGIs.append(item["figi"])
1927                uniquePendingOrders[item["figi"]] = instrument
1928
1929            else:
1930                instrument = uniquePendingOrders[item["figi"]]
1931
1932            if instrument:
1933                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1934                orderType = TKS_ORDER_TYPES[item["orderType"]]
1935                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1936                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1937
1938                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1939                if item["direction"] == "ORDER_DIRECTION_BUY":
1940                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1941
1942                else:
1943                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1944
1945                # requested price for order execution:
1946                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1947
1948                # necessary changes in percent to reach target from current price:
1949                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1950
1951                view["stat"]["orders"].append({
1952                    "orderID": item["orderId"],  # orderId number parameter of current order
1953                    "figi": item["figi"],  # FIGI identification
1954                    "ticker": instrument["ticker"],  # ticker name by FIGI
1955                    "lotsRequested": item["lotsRequested"],  # requested lots value
1956                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1957                    "currentPrice": lastPrice,  # current instrument's price for defined action
1958                    "targetPrice": target,  # requested price for order execution in base currency
1959                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1960                    "percentChanges": changes,  # changes in percent to target from current price
1961                    "currency": item["currency"],  # instrument's currency name
1962                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1963                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1964                    "status": orderState,  # order status from TKS_ORDER_STATES
1965                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1966                })
1967
1968        # --- stop orders sector data:
1969        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1970        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1971
1972        for item in view["raw"]["stopOrders"]:
1973            self._figi = item["figi"]
1974
1975            if item["figi"] not in uniqueStopOrdersFIGIs:
1976                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1977
1978                uniqueStopOrdersFIGIs.append(item["figi"])
1979                uniqueStopOrders[item["figi"]] = instrument
1980
1981            else:
1982                instrument = uniqueStopOrders[item["figi"]]
1983
1984            if instrument:
1985                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1986                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1987                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1988
1989                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1990                if "expirationTime" in item.keys():
1991                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1992                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1993
1994                else:
1995                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1996                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1997
1998                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1999                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
2000                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
2001
2002                else:
2003                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2004
2005                # requested price when stop-order executed:
2006                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2007
2008                # price for limit-order, set up when stop-order executed:
2009                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2010
2011                # necessary changes in percent to reach target from current price:
2012                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2013
2014                view["stat"]["stopOrders"].append({
2015                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2016                    "figi": item["figi"],  # FIGI identification
2017                    "ticker": instrument["ticker"],  # ticker name by FIGI
2018                    "lotsRequested": item["lotsRequested"],  # requested lots value
2019                    "currentPrice": lastPrice,  # current instrument's price for defined action
2020                    "targetPrice": target,  # requested price for stop-order execution in base currency
2021                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2022                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2023                    "percentChanges": changes,  # changes in percent to target from current price
2024                    "currency": item["currency"],  # instrument's currency name
2025                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2026                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2027                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2028                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2029                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2030                })
2031
2032        # --- calculating data for analytics section:
2033        # portfolio distribution by assets:
2034        view["analytics"]["distrByAssets"] = {
2035            "Ruble": {
2036                "uniques": 1,
2037                "cost": view["stat"]["availableRUB"],
2038                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2039            },
2040            "Currencies": {
2041                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2042                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2043                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2044            },
2045            "Shares": {
2046                "uniques": len(view["stat"]["Shares"]),
2047                "cost": view["stat"]["sharesCostRUB"],
2048                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2049            },
2050            "Bonds": {
2051                "uniques": len(view["stat"]["Bonds"]),
2052                "cost": view["stat"]["bondsCostRUB"],
2053                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2054            },
2055            "Etfs": {
2056                "uniques": len(view["stat"]["Etfs"]),
2057                "cost": view["stat"]["etfsCostRUB"],
2058                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2059            },
2060            "Futures": {
2061                "uniques": len(view["stat"]["Futures"]),
2062                "cost": view["stat"]["futuresCostRUB"],
2063                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2064            },
2065        }
2066
2067        # portfolio distribution by companies:
2068        view["analytics"]["distrByCompanies"]["All money cash"] = {
2069            "ticker": "",
2070            "cost": view["stat"]["allCurrenciesCostRUB"],
2071            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2072        }
2073        view["analytics"]["distrByCompanies"].update(byComp)
2074
2075        # portfolio distribution by sectors:
2076        view["analytics"]["distrBySectors"]["All money cash"] = {
2077            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2078            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2079        }
2080        view["analytics"]["distrBySectors"].update(bySect)
2081
2082        # portfolio distribution by currencies:
2083        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2084            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2085
2086            if self.moreDebug:
2087                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2088
2089        view["analytics"]["distrByCurrencies"].update(byCurr)
2090        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2091        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2092
2093        # portfolio distribution by countries:
2094        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2095            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2096
2097            if self.moreDebug:
2098                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2099
2100        view["analytics"]["distrByCountries"].update(byCountry)
2101        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2102        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2103
2104        # --- Prepare text statistics overview in human-readable:
2105        if show:
2106            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2107
2108            # Whatever the value `details`, header not changes:
2109            info = [
2110                "# Client's portfolio\n\n",
2111                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2112                "* **Account ID:** [{}]\n".format(self.accountId),
2113            ]
2114
2115            if details in ["full", "positions", "digest"]:
2116                info.extend([
2117                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2118                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2119                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2120                        view["stat"]["totalChangesRUB"],
2121                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2122                        view["stat"]["totalChangesPercentRUB"],
2123                    ),
2124                ])
2125
2126            if details in ["full", "positions"]:
2127                info.extend([
2128                    "## Open positions\n\n",
2129                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2130                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2131                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2132                        "{:.2f} ({:.2f}) rub".format(
2133                            view["stat"]["availableRUB"],
2134                            view["stat"]["blockedRUB"],
2135                        )
2136                    )
2137                ])
2138
2139                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2140                    return [
2141                        "|                             |                                 |          |              |              |                     |                              |\n",
2142                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2143                            noTradeStr if noTradeStr else typeStr,
2144                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2145                        ),
2146                    ]
2147
2148                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2149                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2150                        "{} [{}]".format(data["ticker"], data["figi"]),
2151                        "{:.2f} ({:.2f}) {}".format(
2152                            data["volume"],
2153                            data["blocked"],
2154                            data["currency"],
2155                        ) if isCurr else "{:.0f} ({:.0f})".format(
2156                            data["volume"],
2157                            data["blocked"],
2158                        ),
2159                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2160                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2161                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2162                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2163                        "{}{:.2f} {} ({}{:.2f}%)".format(
2164                            "+" if data["profit"] > 0 else "",
2165                            data["profit"], data["baseCurrencyName"],
2166                            "+" if data["percentProfit"] > 0 else "",
2167                            data["percentProfit"],
2168                        ),
2169                    )
2170
2171                # --- Show currencies section:
2172                if view["stat"]["Currencies"]:
2173                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2174                    for item in view["stat"]["Currencies"]:
2175                        info.append(_InfoStr(item, isCurr=True))
2176
2177                else:
2178                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2179
2180                # --- Show shares section:
2181                if view["stat"]["Shares"]:
2182                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2183
2184                    for item in view["stat"]["Shares"]:
2185                        info.append(_InfoStr(item))
2186
2187                else:
2188                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2189
2190                # --- Show bonds section:
2191                if view["stat"]["Bonds"]:
2192                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2193
2194                    for item in view["stat"]["Bonds"]:
2195                        info.append(_InfoStr(item))
2196
2197                else:
2198                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2199
2200                # --- Show etfs section:
2201                if view["stat"]["Etfs"]:
2202                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2203
2204                    for item in view["stat"]["Etfs"]:
2205                        info.append(_InfoStr(item))
2206
2207                else:
2208                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2209
2210                # --- Show futures section:
2211                if view["stat"]["Futures"]:
2212                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2213
2214                    for item in view["stat"]["Futures"]:
2215                        info.append(_InfoStr(item))
2216
2217                else:
2218                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2219
2220            if details in ["full", "orders"]:
2221                # --- Show pending limit orders section:
2222                if view["stat"]["orders"]:
2223                    info.extend([
2224                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2225                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2226                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2227                    ])
2228
2229                    for item in view["stat"]["orders"]:
2230                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2231                            "{} [{}]".format(item["ticker"], item["figi"]),
2232                            item["orderID"],
2233                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2234                            "{} {} ({}{:.2f}%)".format(
2235                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2236                                item["baseCurrencyName"],
2237                                "+" if item["percentChanges"] > 0 else "",
2238                                float(item["percentChanges"]),
2239                            ),
2240                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2241                            item["action"],
2242                            item["type"],
2243                            item["date"],
2244                        ))
2245
2246                else:
2247                    info.append("\n## Total pending limit-orders: [0]\n")
2248
2249                # --- Show stop orders section:
2250                if view["stat"]["stopOrders"]:
2251                    info.extend([
2252                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2253                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2254                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2255                    ])
2256
2257                    for item in view["stat"]["stopOrders"]:
2258                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2259                            "{} [{}]".format(item["ticker"], item["figi"]),
2260                            item["orderID"],
2261                            item["lotsRequested"],
2262                            "{} {} ({}{:.2f}%)".format(
2263                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2264                                item["baseCurrencyName"],
2265                                "+" if item["percentChanges"] > 0 else "",
2266                                float(item["percentChanges"]),
2267                            ),
2268                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2269                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2270                            item["action"],
2271                            item["type"],
2272                            item["expType"],
2273                            item["createDate"],
2274                            item["expDate"],
2275                        ))
2276
2277                else:
2278                    info.append("\n## Total stop-orders: [0]\n")
2279
2280            if details in ["full", "analytics"]:
2281                # -- Show analytics section:
2282                if view["stat"]["portfolioCostRUB"] > 0:
2283                    info.extend([
2284                        "\n# Analytics\n\n"
2285                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2286                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2287                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2288                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2289                            view["stat"]["totalChangesRUB"],
2290                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2291                            view["stat"]["totalChangesPercentRUB"],
2292                        ),
2293                        "\n## Portfolio distribution by assets\n"
2294                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2295                        "|------------------------------------|---------|---------|--------------------|\n",
2296                    ])
2297
2298                    for key in view["analytics"]["distrByAssets"].keys():
2299                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2300                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2301                                key,
2302                                view["analytics"]["distrByAssets"][key]["uniques"],
2303                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2304                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2305                            ))
2306
2307                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2308
2309                    info.extend([
2310                        "\n## Portfolio distribution by companies\n"
2311                        "\n| Company                                      | Percent | Current cost       |\n",
2312                        aSepLine,
2313                    ])
2314
2315                    for company in view["analytics"]["distrByCompanies"].keys():
2316                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2317                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2318                                "{}{}".format(
2319                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2320                                    company,
2321                                ),
2322                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2323                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2324                            ))
2325
2326                    info.extend([
2327                        "\n## Portfolio distribution by sectors\n"
2328                        "\n| Sector                                       | Percent | Current cost       |\n",
2329                        aSepLine,
2330                    ])
2331
2332                    for sector in view["analytics"]["distrBySectors"].keys():
2333                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2334                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2335                                sector,
2336                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2337                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2338                            ))
2339
2340                    info.extend([
2341                        "\n## Portfolio distribution by currencies\n"
2342                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2343                        aSepLine,
2344                    ])
2345
2346                    for curr in view["analytics"]["distrByCurrencies"].keys():
2347                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2348                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2349                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2350                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2351                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2352                            ))
2353
2354                    info.extend([
2355                        "\n## Portfolio distribution by countries\n"
2356                        "\n| Assets by country                            | Percent | Current cost       |\n",
2357                        aSepLine,
2358                    ])
2359
2360                    for country in view["analytics"]["distrByCountries"].keys():
2361                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2362                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2363                                country,
2364                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2365                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2366                            ))
2367
2368            if details in ["full", "calendar"]:
2369                # -- Show bonds payment calendar section:
2370                if view["stat"]["Bonds"]:
2371                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2372                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2373                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2374
2375                else:
2376                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2377
2378            infoText = "".join(info)
2379
2380            uLogger.info(infoText)
2381
2382            if details == "full" and self.overviewFile:
2383                filename = self.overviewFile
2384
2385            elif details == "digest" and self.overviewDigestFile:
2386                filename = self.overviewDigestFile
2387
2388            elif details == "positions" and self.overviewPositionsFile:
2389                filename = self.overviewPositionsFile
2390
2391            elif details == "orders" and self.overviewOrdersFile:
2392                filename = self.overviewOrdersFile
2393
2394            elif details == "analytics" and self.overviewAnalyticsFile:
2395                filename = self.overviewAnalyticsFile
2396
2397            elif details == "calendar" and self.overviewBondsCalendarFile:
2398                filename = self.overviewBondsCalendarFile
2399
2400            else:
2401                filename = ""
2402
2403            if filename:
2404                with open(filename, "w", encoding="UTF-8") as fH:
2405                    fH.write(infoText)
2406
2407                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2408
2409                if self.useHTMLReports:
2410                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2411                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2412                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2413
2414                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2415
2416        return view
2417
2418    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2419        """
2420        Returns history operations between two given dates for current `accountId`.
2421        If `reportFile` string is not empty then also save human-readable report.
2422        Shows some statistical data of closed positions.
2423
2424        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2425        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2426        :param show: if `True` then also prints all records to the console.
2427        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2428        :return: original list of dictionaries with history of deals records from API ("operations" key):
2429                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2430                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2431        """
2432        if self.accountId is None or not self.accountId:
2433            uLogger.error("Variable `accountId` must be defined for using this method!")
2434            raise Exception("Account ID required")
2435
2436        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2437
2438        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2439
2440        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2441        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2442        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2443        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2444        customStat = {}  # custom statistics in additional to responseJSON
2445
2446        # --- output report in human-readable format:
2447        if show or self.reportFile:
2448            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2449            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2450            nextDay = ""
2451
2452            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2453
2454            if len(ops) > 0:
2455                customStat = {
2456                    "opsCount": 0,  # total operations count
2457                    "buyCount": 0,  # buy operations
2458                    "sellCount": 0,  # sell operations
2459                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2460                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2461                    "payIn": {"rub": 0.},  # Deposit brokerage account
2462                    "payOut": {"rub": 0.},  # Withdrawals
2463                    "divs": {"rub": 0.},  # Dividends income
2464                    "coupons": {"rub": 0.},  # Coupon's income
2465                    "brokerCom": {"rub": 0.},  # Service commissions
2466                    "serviceCom": {"rub": 0.},  # Service commissions
2467                    "marginCom": {"rub": 0.},  # Margin commissions
2468                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2469                }
2470
2471                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2472                for item in ops:
2473                    if item["state"] == "OPERATION_STATE_EXECUTED":
2474                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2475
2476                        # count buy operations:
2477                        if "_BUY" in item["operationType"]:
2478                            customStat["buyCount"] += 1
2479
2480                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2481                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2482
2483                            else:
2484                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2485
2486                        # count sell operations:
2487                        elif "_SELL" in item["operationType"]:
2488                            customStat["sellCount"] += 1
2489
2490                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2491                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2492
2493                            else:
2494                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2495
2496                        # count incoming operations:
2497                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2498                            if item["payment"]["currency"] in customStat["payIn"].keys():
2499                                customStat["payIn"][item["payment"]["currency"]] += payment
2500
2501                            else:
2502                                customStat["payIn"][item["payment"]["currency"]] = payment
2503
2504                        # count withdrawals operations:
2505                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2506                            if item["payment"]["currency"] in customStat["payOut"].keys():
2507                                customStat["payOut"][item["payment"]["currency"]] += payment
2508
2509                            else:
2510                                customStat["payOut"][item["payment"]["currency"]] = payment
2511
2512                        # count dividends income:
2513                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2514                            if item["payment"]["currency"] in customStat["divs"].keys():
2515                                customStat["divs"][item["payment"]["currency"]] += payment
2516
2517                            else:
2518                                customStat["divs"][item["payment"]["currency"]] = payment
2519
2520                        # count coupon's income:
2521                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2522                            if item["payment"]["currency"] in customStat["coupons"].keys():
2523                                customStat["coupons"][item["payment"]["currency"]] += payment
2524
2525                            else:
2526                                customStat["coupons"][item["payment"]["currency"]] = payment
2527
2528                        # count broker commissions:
2529                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2530                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2531                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2532
2533                            else:
2534                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2535
2536                        # count service commissions:
2537                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2538                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2539                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2540
2541                            else:
2542                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2543
2544                        # count margin commissions:
2545                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2546                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2547                                customStat["marginCom"][item["payment"]["currency"]] += payment
2548
2549                            else:
2550                                customStat["marginCom"][item["payment"]["currency"]] = payment
2551
2552                        # count withholding taxes:
2553                        elif "_TAX" in item["operationType"]:
2554                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2555                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2556
2557                            else:
2558                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2559
2560                        else:
2561                            continue
2562
2563                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2564
2565                # --- view "Actions" lines:
2566                info.extend([
2567                    "| Report sections            |                               |                              |                      |                        |\n",
2568                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2569                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2570                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2571                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2572                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2573                    ),
2574                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2575                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2576                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2577                    ),
2578                ])
2579
2580                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2581                for key in opsKeys:
2582                    if key == "rub":
2583                        continue
2584
2585                    info.extend([
2586                        "|                            |                               | {:<28} |                      |                        |\n".format(
2587                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2588                        ),
2589                        "|                            |                               | {:<28} |                      |                        |\n".format(
2590                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2591                        ),
2592                    ])
2593
2594                info.append(splitLine1)
2595
2596                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2597                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2598                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2599                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2600                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2601                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2602                    )
2603
2604                # --- view "Payments" lines:
2605                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2606                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2607
2608                for key in paymentsKeys:
2609                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2610
2611                info.append(splitLine1)
2612
2613                # --- view "Commissions and taxes" lines:
2614                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2615                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2616
2617                for key in comKeys:
2618                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2619
2620                info.extend([
2621                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2622                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2623                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2624                ])
2625
2626            else:
2627                info.append("Broker returned no operations during this period\n")
2628
2629            # --- view "Operations" section:
2630            for item in ops:
2631                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2632                    continue
2633
2634                else:
2635                    self._figi = item["figi"]
2636                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2637                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2638
2639                    # group of deals during one day:
2640                    if nextDay and item["date"].split("T")[0] != nextDay:
2641                        info.append(splitLine2)
2642                        nextDay = ""
2643
2644                    else:
2645                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2646
2647                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2648                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2649                        self._figi if self._figi else "—",
2650                        instrument["ticker"] if instrument else "—",
2651                        instrument["type"] if instrument else "—",
2652                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2653                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2654                        TKS_OPERATION_STATES[item["state"]],
2655                        TKS_OPERATION_TYPES[item["operationType"]],
2656                    ))
2657
2658            infoText = "".join(info)
2659
2660            if show:
2661                if self.moreDebug:
2662                    uLogger.debug("Records about history of a client's operations successfully received")
2663
2664                uLogger.info(infoText)
2665
2666            if self.reportFile:
2667                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2668                    fH.write(infoText)
2669
2670                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2671
2672                if self.useHTMLReports:
2673                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2674                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2675                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2676
2677                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2678
2679        return ops, customStat
2680
2681    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2682        """
2683        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2684
2685        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2686        Warning! Broker server used ISO UTC time by default.
2687
2688        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2689        Also, `historyFile` used to update history with `onlyMissing` parameter.
2690
2691        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2692
2693        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2694        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2695        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2696                         `"hour"`, `"day"`. Default: `"hour"`.
2697        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2698                            False by default. Warning! History appends only from last candle to current time
2699                            with always update last candle!
2700        :param csvSep: separator if csv-file is used, `,` by default.
2701        :param show: if `True` then also prints Pandas DataFrame to the console.
2702        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2703                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2704        """
2705        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2706        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2707        history = None  # empty pandas object for history
2708
2709        if interval not in TKS_CANDLE_INTERVALS.keys():
2710            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2711            raise Exception("Incorrect value")
2712
2713        if not (self._ticker or self._figi):
2714            uLogger.error("Ticker or FIGI must be defined!")
2715            raise Exception("Ticker or FIGI required")
2716
2717        if self._ticker and not self._figi:
2718            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2719            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2720
2721        if self._figi and not self._ticker:
2722            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2723            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2724
2725        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2726        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2727        if interval.lower() != "day":
2728            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2729
2730        delta = dtEnd - dtStart  # current UTC time minus last time in file
2731        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2732
2733        # calculate history length in candles:
2734        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2735        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2736            length += 1  # to avoid fraction time
2737
2738        # calculate data blocks count:
2739        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2740
2741        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2742        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2743        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2744        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2745        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2746
2747        tempOld = None  # pandas object for old history, if --only-missing key present
2748        lastTime = None  # datetime object of last old candle in file
2749
2750        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2751            uLogger.debug("--only-missing key present, add only last missing candles...")
2752            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2753
2754            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2755
2756            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2757            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2758            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2759            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2760
2761            # get last datetime object from last string in file or minus 1 delta if file is empty:
2762            if len(tempOld) > 0:
2763                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2764
2765            else:
2766                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2767
2768            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2769
2770        responseJSONs = []  # raw history blocks of data
2771
2772        blockEnd = dtEnd
2773        for item in range(blocks):
2774            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2775            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2776
2777            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2778                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2779            ))
2780
2781            if blockStart == blockEnd:
2782                uLogger.debug("Skipped this zero-length block...")
2783
2784            else:
2785                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2786                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2787                self.body = str({
2788                    "figi": self._figi,
2789                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2790                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2791                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2792                })
2793                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2794
2795                if "code" in responseJSON.keys():
2796                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2797
2798                else:
2799                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2800                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2801
2802                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2803
2804            blockEnd = blockStart
2805
2806        printCount = len(responseJSONs)  # candles to show in console
2807        if responseJSONs:
2808            tempHistory = pd.DataFrame(
2809                data={
2810                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2811                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2812                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2813                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2814                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2815                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2816                    "volume": [int(item["volume"]) for item in responseJSONs],
2817                },
2818                index=range(len(responseJSONs)),
2819                columns=["date", "time", "open", "high", "low", "close", "volume"],
2820            )
2821            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2822            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2823
2824            # append only newest candles to old history if --only-missing key present:
2825            if onlyMissing and tempOld is not None and lastTime is not None:
2826                index = 0  # find start index in tempHistory data:
2827
2828                for i, item in tempHistory.iterrows():
2829                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2830
2831                    if curTime == lastTime:
2832                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2833                        index = i
2834                        printCount = index + 1
2835                        break
2836
2837                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2838
2839            else:
2840                history = tempHistory  # if no `--only-missing` key then load full data from server
2841
2842            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2843
2844        if history is not None and not history.empty:
2845            if show:
2846                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2847                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2848                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2849                ))
2850
2851        else:
2852            uLogger.warning("Received an empty candles history!")
2853
2854        if self.historyFile is not None:
2855            if history is not None and not history.empty:
2856                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2857                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2858
2859            else:
2860                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2861
2862        else:
2863            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2864
2865        return history
2866
2867    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2868        """
2869        Load candles history from csv-file and return Pandas DataFrame object.
2870
2871        See also: `History()` and `ShowHistoryChart()` methods.
2872
2873        :param filePath: path to csv-file to open.
2874        """
2875        loadedHistory = None  # init candles data object
2876
2877        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2878
2879        if os.path.exists(filePath):
2880            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2881
2882            tfStr = self.priceModel.FormattedDelta(
2883                self.priceModel.timeframe,
2884                "{days} days {hours}h {minutes}m {seconds}s",
2885            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2886                self.priceModel.timeframe,
2887                "{hours}h {minutes}m {seconds}s",
2888            )
2889
2890            if loadedHistory is not None and not loadedHistory.empty:
2891                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2892                    len(loadedHistory),
2893                    tfStr,
2894                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2895                )
2896
2897            else:
2898                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2899
2900        else:
2901            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2902
2903        return loadedHistory
2904
2905    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2906        """
2907        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2908
2909        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2910        Default: `index.html` (both for interact and non-interact candlesticks chart).
2911
2912        See also: `History()` and `LoadHistory()` methods.
2913
2914        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2915        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2916                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2917                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2918                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2919        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2920                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2921        """
2922        if isinstance(candles, str):
2923            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2924            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2925
2926        elif isinstance(candles, pd.DataFrame):
2927            self.priceModel.prices = candles  # set candles chain from variable
2928            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2929
2930            if "datetime" not in candles.columns:
2931                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2932
2933        else:
2934            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2935            raise Exception("Incorrect value")
2936
2937        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2938
2939        if interact:
2940            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2941
2942            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2943
2944        else:
2945            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2946
2947            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2948
2949        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
2950
2951    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2952        """
2953        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2954        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2955
2956        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2957
2958        :param operation: string "Buy" or "Sell".
2959        :param lots: volume, integer count of lots >= 1.
2960        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2961        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2962        :param expDate: string "Undefined" by default or local date in future,
2963                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2964        :return: JSON with response from broker server.
2965        """
2966        if self.accountId is None or not self.accountId:
2967            uLogger.error("Variable `accountId` must be defined for using this method!")
2968            raise Exception("Account ID required")
2969
2970        if operation is None or not operation or operation not in ("Buy", "Sell"):
2971            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2972            raise Exception("Incorrect value")
2973
2974        if lots is None or lots < 1:
2975            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2976            lots = 1
2977
2978        if tp is None or tp < 0:
2979            tp = 0
2980
2981        if sl is None or sl < 0:
2982            sl = 0
2983
2984        if expDate is None or not expDate:
2985            expDate = "Undefined"
2986
2987        if not (self._ticker or self._figi):
2988            uLogger.error("Ticker or FIGI must be defined!")
2989            raise Exception("Ticker or FIGI required")
2990
2991        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2992        self._ticker = instrument["ticker"]
2993        self._figi = instrument["figi"]
2994
2995        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
2996
2997        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2998        self.body = str({
2999            "figi": self._figi,
3000            "quantity": str(lots),
3001            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3002            "accountId": str(self.accountId),
3003            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3004        })
3005        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3006
3007        if "orderId" in response.keys():
3008            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3009                operation, response["orderId"],
3010                self._ticker, self._figi, lots,
3011                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3012                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3013                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3014            ))
3015
3016            if tp > 0:
3017                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3018
3019            if sl > 0:
3020                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3021
3022        else:
3023            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3024
3025        return response
3026
3027    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3028        """
3029        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3030        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3031
3032        See also: `Order()` and `Trade()` docstrings.
3033
3034        :param lots: volume, integer count of lots >= 1.
3035        :param tp: float > 0, take profit price of stop-order.
3036        :param sl: float > 0, stop loss price of stop-order.
3037        :param expDate: it's a local date in future.
3038                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3039        :return: JSON with response from broker server.
3040        """
3041        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
3042
3043    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3044        """
3045        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3046        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3047
3048        See also: `Order()` and `Trade()` docstrings.
3049
3050        :param lots: volume, integer count of lots >= 1.
3051        :param tp: float > 0, take profit price of stop-order.
3052        :param sl: float > 0, stop loss price of stop-order.
3053        :param expDate: it's a local date in the future.
3054                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3055        :return: JSON with response from broker server.
3056        """
3057        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
3058
3059    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3060        """
3061        Close position of given instruments.
3062
3063        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3064        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3065                         This avoids unnecessary downloading data from the server.
3066        """
3067        if instruments is None or not instruments:
3068            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3069            raise Exception("Ticker or FIGI required")
3070
3071        if isinstance(instruments, str):
3072            instruments = [instruments]
3073
3074        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3075        if uniqueInstruments:
3076            if portfolio is None or not portfolio:
3077                portfolio = self.Overview(show=False)
3078
3079            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3080            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3081
3082            for self._figi in uniqueInstruments:
3083                if self._figi not in allOpened:
3084                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3085                    continue
3086
3087                # search open trade info about instrument by ticker:
3088                instrument = {}
3089                for iType in TKS_INSTRUMENTS:
3090                    if instrument:
3091                        break
3092
3093                    for item in portfolio["stat"][iType]:
3094                        if item["figi"] == self._figi:
3095                            instrument = item
3096                            break
3097
3098                if instrument:
3099                    self._ticker = instrument["ticker"]
3100                    self._figi = instrument["figi"]
3101
3102                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3103                        self._ticker,
3104                        self._figi,
3105                        int(instrument["volume"]),
3106                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3107                    ))
3108
3109                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3110
3111                    if tradeLots > 0:
3112                        if instrument["blocked"] > 0:
3113                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3114                                instrument["blocked"],
3115                                self._ticker,
3116                                tradeLots,
3117                            ))
3118
3119                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3120                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3121
3122                    else:
3123                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))
3124
3125    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3126        """
3127        Close all positions of given instruments with defined type.
3128
3129        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3130        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3131                         This avoids unnecessary downloading data from the server.
3132        """
3133        if iType not in TKS_INSTRUMENTS:
3134            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3135
3136        else:
3137            if portfolio is None or not portfolio:
3138                portfolio = self.Overview(show=False)
3139
3140            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3141            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3142
3143            if tickers and portfolio:
3144                self.CloseTrades(tickers, portfolio)
3145
3146            else:
3147                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
3148
3149    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3150        """
3151        Universal method to create market or limit orders with all available parameters for current `accountId`.
3152        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3153
3154        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3155        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3156
3157        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3158        then broker immediately open market order as you can do simple --buy or --sell operations!
3159
3160        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3161        When current price will go up or down to target price value then broker opens a limit order.
3162        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3163
3164        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3165
3166        :param operation: string "Buy" or "Sell".
3167        :param orderType: string "Limit" or "Stop".
3168        :param lots: volume, integer count of lots >= 1.
3169        :param targetPrice: target price > 0. This is open trade price for limit order.
3170        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3171                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3172        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3173                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3174                         Stop loss order always executed by market price.
3175        :param expDate: string "Undefined" by default or local date in future.
3176                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3177                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3178                        A limit order has no expiration date, it lasts until the end of the trading day.
3179        :return: JSON with response from broker server.
3180        """
3181        if self.accountId is None or not self.accountId:
3182            uLogger.error("Variable `accountId` must be defined for using this method!")
3183            raise Exception("Account ID required")
3184
3185        if operation is None or not operation or operation not in ("Buy", "Sell"):
3186            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3187            raise Exception("Incorrect value")
3188
3189        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3190            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3191            raise Exception("Incorrect value")
3192
3193        if lots is None or lots < 1:
3194            uLogger.error("You must define trade volume > 0: integer count of lots!")
3195            raise Exception("Incorrect value")
3196
3197        if targetPrice is None or targetPrice <= 0:
3198            uLogger.error("Target price for limit-order must be greater than 0!")
3199            raise Exception("Incorrect value")
3200
3201        if limitPrice is None or limitPrice <= 0:
3202            limitPrice = targetPrice
3203
3204        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3205            stopType = "Limit"
3206
3207        if expDate is None or not expDate:
3208            expDate = "Undefined"
3209
3210        if not (self._ticker or self._figi):
3211            uLogger.error("Tocker or FIGI must be defined!")
3212            raise Exception("Ticker or FIGI required")
3213
3214        response = {}
3215        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3216        self._ticker = instrument["ticker"]
3217        self._figi = instrument["figi"]
3218
3219        if orderType == "Limit":
3220            uLogger.debug(
3221                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3222                    self._ticker, self._figi,
3223                    operation, lots, targetPrice, instrument["currency"],
3224                ))
3225
3226            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3227            self.body = str({
3228                "figi": self._figi,
3229                "quantity": str(lots),
3230                "price": FloatToNano(targetPrice),
3231                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3232                "accountId": str(self.accountId),
3233                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3234            })
3235            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3236
3237            if "orderId" in response.keys():
3238                uLogger.info(
3239                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3240                        response["orderId"], self._ticker, self._figi, operation, lots,
3241                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3242                    ))
3243
3244                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3245                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3246                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3247                            targetPrice, instrument["currency"],
3248                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3249                        ))
3250
3251                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3252                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3253                            targetPrice, instrument["currency"],
3254                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3255                        ))
3256
3257            else:
3258                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3259
3260        if orderType == "Stop":
3261            uLogger.debug(
3262                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3263                    self._ticker, self._figi,
3264                    operation, lots,
3265                    targetPrice, instrument["currency"],
3266                    limitPrice, instrument["currency"],
3267                    stopType, expDate,
3268                ))
3269
3270            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3271            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3272            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3273
3274            body = {
3275                "figi": self._figi,
3276                "quantity": str(lots),
3277                "price": FloatToNano(limitPrice),
3278                "stopPrice": FloatToNano(targetPrice),
3279                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3280                "accountId": str(self.accountId),
3281                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3282                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3283            }
3284
3285            if expDateUTC:
3286                body["expireDate"] = expDateUTC
3287
3288            self.body = str(body)
3289            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3290
3291            if "stopOrderId" in response.keys():
3292                uLogger.info(
3293                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3294                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3295                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3296                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3297                        TKS_STOP_ORDER_TYPES[stopOrderType],
3298                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3299                    ))
3300
3301                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3302                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3303                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3304                            targetPrice, instrument["currency"],
3305                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3306                        ))
3307
3308                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3309                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3310                            targetPrice, instrument["currency"],
3311                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3312                        ))
3313
3314            else:
3315                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3316
3317        return response
3318
3319    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3320        """
3321        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3322        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3323        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3324        See also: `Order()` docstring.
3325
3326        :param lots: volume, integer count of lots >= 1.
3327        :param targetPrice: target price > 0. This is open trade price for limit order.
3328        :return: JSON with response from broker server.
3329        """
3330        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
3331
3332    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3333        """
3334        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3335        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3336        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3337        target price value then broker opens a limit order. See also: `Order()` docstring.
3338
3339        :param lots: volume, integer count of lots >= 1.
3340        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3341        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3342                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3343        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3344                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3345        :param expDate: string "Undefined" by default or local date in future.
3346                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3347                        This date is converting to UTC format for server.
3348        :return: JSON with response from broker server.
3349        """
3350        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3351
3352    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3353        """
3354        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3355        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3356        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3357        See also: `Order()` docstring.
3358
3359        :param lots: volume, integer count of lots >= 1.
3360        :param targetPrice: target price > 0. This is open trade price for limit order.
3361        :return: JSON with response from broker server.
3362        """
3363        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
3364
3365    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3366        """
3367        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3368        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3369        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3370        target price value then broker opens a limit order. See also: `Order()` docstring.
3371
3372        :param lots: volume, integer count of lots >= 1.
3373        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3374        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3375                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3376        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3377                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3378        :param expDate: string "Undefined" by default or local date in future.
3379                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3380                        This date is converting to UTC format for server.
3381        :return: JSON with response from broker server.
3382        """
3383        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
3384
3385    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3386        """
3387        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3388
3389        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3390        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3391                             This avoids unnecessary downloading data from the server.
3392        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3393        """
3394        if self.accountId is None or not self.accountId:
3395            uLogger.error("Variable `accountId` must be defined for using this method!")
3396            raise Exception("Account ID required")
3397
3398        if orderIDs:
3399            if allOrdersIDs is None:
3400                rawOrders = self.RequestPendingOrders()
3401                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3402
3403            if allStopOrdersIDs is None:
3404                rawStopOrders = self.RequestStopOrders()
3405                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3406
3407            for orderID in orderIDs:
3408                idInPendingOrders = orderID in allOrdersIDs
3409                idInStopOrders = orderID in allStopOrdersIDs
3410
3411                if not (idInPendingOrders or idInStopOrders):
3412                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3413                    continue
3414
3415                else:
3416                    if idInPendingOrders:
3417                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3418
3419                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3420                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3421                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3422                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3423
3424                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3425                            if self.moreDebug:
3426                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3427
3428                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3429
3430                        else:
3431                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3432
3433                    elif idInStopOrders:
3434                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3435
3436                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3437                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3438                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3439                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3440
3441                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3442                            if self.moreDebug:
3443                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3444
3445                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3446
3447                        else:
3448                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3449
3450                    else:
3451                        continue
3452
3453    def CloseAllOrders(self) -> None:
3454        """
3455        Gets a list of open pending and stop orders and cancel it all.
3456        """
3457        rawOrders = self.RequestPendingOrders()
3458        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3459        lenOrders = len(allOrdersIDs)
3460
3461        rawStopOrders = self.RequestStopOrders()
3462        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3463        lenSOrders = len(allStopOrdersIDs)
3464
3465        if lenOrders > 0 or lenSOrders > 0:
3466            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3467
3468            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3469
3470        else:
3471            uLogger.info("Orders not found, nothing to cancel.")
3472
3473    def CloseAll(self, *args) -> None:
3474        """
3475        Close all available (not blocked) opened trades and orders.
3476
3477        Also, you can select one or more keywords case-insensitive:
3478        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3479
3480        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3481        """
3482        overview = self.Overview(show=False)  # get all open trades info
3483
3484        if len(args) == 0:
3485            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3486            self.CloseAllOrders()  # close all pending and stop orders
3487
3488            for iType in TKS_INSTRUMENTS:
3489                if iType != "Currencies":
3490                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3491
3492        else:
3493            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3494            lowerArgs = [x.lower() for x in args]
3495
3496            if "orders" in lowerArgs:
3497                self.CloseAllOrders()  # close all pending and stop orders
3498
3499            for iType in TKS_INSTRUMENTS:
3500                if iType.lower() in lowerArgs and iType != "Currencies":
3501                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3502
3503    def CloseAllByTicker(self, instrument: str) -> None:
3504        """
3505        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3506
3507        This method searches opened trade and orders of instrument throw all portfolio and then use
3508        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3509
3510        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3511
3512        :param instrument: string with ticker.
3513        """
3514        if instrument is None or not instrument:
3515            uLogger.error("Ticker name must be defined for using this method!")
3516            raise Exception("Ticker required")
3517
3518        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3519
3520        self._ticker = instrument  # try to set instrument as ticker
3521        self._figi = ""
3522
3523        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3524        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3525
3526        if limitAll and self.IsInLimitOrders(portfolio=overview):
3527            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3528            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3529
3530        if stopAll and self.IsInStopOrders(portfolio=overview):
3531            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3532            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3533
3534        if self.IsInPortfolio(portfolio=overview):
3535            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3536            self.CloseTrades(instruments=[instrument], portfolio=overview)
3537
3538    def CloseAllByFIGI(self, instrument: str) -> None:
3539        """
3540        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3541
3542        This method searches opened trade and orders of instrument throw all portfolio and then use
3543        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3544
3545        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3546
3547        :param instrument: string with FIGI id.
3548        """
3549        if instrument is None or not instrument:
3550            uLogger.error("FIGI id must be defined for using this method!")
3551            raise Exception("FIGI required")
3552
3553        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3554
3555        self._ticker = ""
3556        self._figi = instrument  # try to set instrument as FIGI id
3557
3558        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3559        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3560
3561        if limitAll and self.IsInLimitOrders(portfolio=overview):
3562            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3563            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3564
3565        if stopAll and self.IsInStopOrders(portfolio=overview):
3566            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3567            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3568
3569        if self.IsInPortfolio(portfolio=overview):
3570            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3571            self.CloseTrades(instruments=[instrument], portfolio=overview)
3572
3573    @staticmethod
3574    def ParseOrderParameters(operation, **inputParameters):
3575        """
3576        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3577
3578        :param operation: string "Buy" or "Sell".
3579        :param inputParameters: this is dict of strings that looks like this
3580               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3581               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3582               "prices" key: one or more prices to open limit-orders
3583               Counts of values in lots and prices lists must be equals!
3584        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3585        """
3586        # TODO: update order grid work with api v2
3587        pass
3588        # uLogger.debug("Input parameters: {}".format(inputParameters))
3589        #
3590        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3591        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3592        #     raise Exception("Incorrect value")
3593        #
3594        # if "l" in inputParameters.keys():
3595        #     inputParameters["lots"] = inputParameters.pop("l")
3596        #
3597        # if "p" in inputParameters.keys():
3598        #     inputParameters["prices"] = inputParameters.pop("p")
3599        #
3600        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3601        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3602        #     raise Exception("Incorrect value")
3603        #
3604        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3605        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3606        #
3607        # if len(lots) != len(prices):
3608        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3609        #     raise Exception("Incorrect value")
3610        #
3611        # uLogger.debug("Extracted parameters for orders:")
3612        # uLogger.debug("lots = {}".format(lots))
3613        # uLogger.debug("prices = {}".format(prices))
3614        #
3615        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3616        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3617        # uLogger.debug("Order parameters: {}".format(result))
3618        #
3619        # return result
3620
3621    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3622        """
3623        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3624
3625        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3626        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3627        """
3628        result = False
3629        msg = "Instrument not defined!"
3630
3631        if portfolio is None or not portfolio:
3632            portfolio = self.Overview(show=False)
3633
3634        if self._ticker:
3635            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3636            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3637
3638            for iType in TKS_INSTRUMENTS:
3639                for instrument in portfolio["stat"][iType]:
3640                    if instrument["ticker"] == self._ticker:
3641                        result = True
3642                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3643                        break
3644
3645        elif self._figi:
3646            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3647            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3648
3649            for iType in TKS_INSTRUMENTS:
3650                for instrument in portfolio["stat"][iType]:
3651                    if instrument["figi"] == self._figi:
3652                        result = True
3653                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3654                        break
3655
3656        else:
3657            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3658
3659        uLogger.debug(msg)
3660
3661        return result
3662
3663    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3664        """
3665        Returns instrument from the user's portfolio if it presents there.
3666        Instrument must be defined by `ticker` (highly priority) or `figi`.
3667
3668        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3669        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3670        """
3671        result = None
3672        msg = "Instrument not defined!"
3673
3674        if portfolio is None or not portfolio:
3675            portfolio = self.Overview(show=False)
3676
3677        if self._ticker:
3678            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3679            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3680
3681            for iType in TKS_INSTRUMENTS:
3682                for instrument in portfolio["stat"][iType]:
3683                    if instrument["ticker"] == self._ticker:
3684                        result = instrument
3685                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3686                        break
3687
3688        elif self._figi:
3689            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3690            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3691
3692            for iType in TKS_INSTRUMENTS:
3693                for instrument in portfolio["stat"][iType]:
3694                    if instrument["figi"] == self._figi:
3695                        result = instrument
3696                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3697                        break
3698
3699        else:
3700            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3701
3702        uLogger.debug(msg)
3703
3704        return result
3705
3706    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3707        """
3708        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3709
3710        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3711
3712        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3713        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3714        """
3715        result = False
3716        msg = "Instrument not defined!"
3717
3718        if portfolio is None or not portfolio:
3719            portfolio = self.Overview(show=False)
3720
3721        if self._ticker:
3722            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3723            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3724
3725            for instrument in portfolio["stat"]["orders"]:
3726                if instrument["ticker"] == self._ticker:
3727                    result = True
3728                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3729                    break
3730
3731        elif self._figi:
3732            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3733            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3734
3735            for instrument in portfolio["stat"]["orders"]:
3736                if instrument["figi"] == self._figi:
3737                    result = True
3738                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3739                    break
3740
3741        else:
3742            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3743
3744        uLogger.debug(msg)
3745
3746        return result
3747
3748    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3749        """
3750        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3751        Instrument must be defined by `ticker` (highly priority) or `figi`.
3752
3753        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3754
3755        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3756        :return: list with `orderID`s of limit orders.
3757        """
3758        result = []
3759        msg = "Instrument not defined!"
3760
3761        if portfolio is None or not portfolio:
3762            portfolio = self.Overview(show=False)
3763
3764        if self._ticker:
3765            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3766            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3767
3768            for instrument in portfolio["stat"]["orders"]:
3769                if instrument["ticker"] == self._ticker:
3770                    result.append(instrument["orderID"])
3771
3772            if result:
3773                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3774
3775        elif self._figi:
3776            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3777            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3778
3779            for instrument in portfolio["stat"]["orders"]:
3780                if instrument["figi"] == self._figi:
3781                    result.append(instrument["orderID"])
3782
3783            if result:
3784                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3785
3786        else:
3787            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3788
3789        uLogger.debug(msg)
3790
3791        return result
3792
3793    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3794        """
3795        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3796
3797        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3798
3799        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3800        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3801        """
3802        result = False
3803        msg = "Instrument not defined!"
3804
3805        if portfolio is None or not portfolio:
3806            portfolio = self.Overview(show=False)
3807
3808        if self._ticker:
3809            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3810            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3811
3812            for instrument in portfolio["stat"]["stopOrders"]:
3813                if instrument["ticker"] == self._ticker:
3814                    result = True
3815                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3816                    break
3817
3818        elif self._figi:
3819            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3820            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3821
3822            for instrument in portfolio["stat"]["stopOrders"]:
3823                if instrument["figi"] == self._figi:
3824                    result = True
3825                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3826                    break
3827
3828        else:
3829            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3830
3831        uLogger.debug(msg)
3832
3833        return result
3834
3835    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3836        """
3837        Returns list with all `orderID`s of opened stop orders for the instrument.
3838        Instrument must be defined by `ticker` (highly priority) or `figi`.
3839
3840        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3841
3842        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3843        :return: list with `orderID`s of stop orders.
3844        """
3845        result = []
3846        msg = "Instrument not defined!"
3847
3848        if portfolio is None or not portfolio:
3849            portfolio = self.Overview(show=False)
3850
3851        if self._ticker:
3852            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3853            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3854
3855            for instrument in portfolio["stat"]["stopOrders"]:
3856                if instrument["ticker"] == self._ticker:
3857                    result.append(instrument["orderID"])
3858
3859            if result:
3860                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3861
3862        elif self._figi:
3863            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3864            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3865
3866            for instrument in portfolio["stat"]["stopOrders"]:
3867                if instrument["figi"] == self._figi:
3868                    result.append(instrument["orderID"])
3869
3870            if result:
3871                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3872
3873        else:
3874            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3875
3876        uLogger.debug(msg)
3877
3878        return result
3879
3880    def RequestLimits(self) -> dict:
3881        """
3882        Method for obtaining the available funds for withdrawal for current `accountId`.
3883
3884        See also:
3885        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3886        - `OverviewLimits()` method
3887
3888        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3889                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3890                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3891                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3892        """
3893        if self.accountId is None or not self.accountId:
3894            uLogger.error("Variable `accountId` must be defined for using this method!")
3895            raise Exception("Account ID required")
3896
3897        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3898
3899        self.body = str({"accountId": self.accountId})
3900        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3901        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3902
3903        if self.moreDebug:
3904            uLogger.debug("Records about available funds for withdrawal successfully received")
3905
3906        return rawLimits
3907
3908    def OverviewLimits(self, show: bool = False) -> dict:
3909        """
3910        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3911
3912        See also: `RequestLimits()`.
3913
3914        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3915        :return: dict with raw parsed data from server and some calculated statistics about it.
3916        """
3917        if self.accountId is None or not self.accountId:
3918            uLogger.error("Variable `accountId` must be defined for using this method!")
3919            raise Exception("Account ID required")
3920
3921        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3922
3923        view = {
3924            "rawLimits": rawLimits,
3925            "limits": {  # parsed data for every currency:
3926                "money": {  # this is an array of portfolio currency positions
3927                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3928                },
3929                "blocked": {  # this is an array of blocked currency
3930                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3931                },
3932                "blockedGuarantee": {  # this is locked money under collateral for futures
3933                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3934                },
3935            },
3936        }
3937
3938        # --- Prepare text table with limits in human-readable format:
3939        if show:
3940            info = [
3941                "# Withdrawal limits\n\n",
3942                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3943                "* **Account ID:** [{}]\n".format(self.accountId),
3944            ]
3945
3946            if view["limits"]["money"]:
3947                info.extend([
3948                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3949                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3950                ])
3951
3952            else:
3953                info.append("\nNo withdrawal limits\n")
3954
3955            for curr in view["limits"]["money"].keys():
3956                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3957                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3958                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3959
3960                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3961                    "[{}]".format(curr),
3962                    "{:.2f}".format(view["limits"]["money"][curr]),
3963                    "{:.2f}".format(availableMoney),
3964                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3965                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3966                )
3967
3968                if curr == "rub":
3969                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3970
3971                else:
3972                    info.append(infoStr)
3973
3974            infoText = "".join(info)
3975
3976            uLogger.info(infoText)
3977
3978            if self.withdrawalLimitsFile:
3979                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3980                    fH.write(infoText)
3981
3982                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3983
3984                if self.useHTMLReports:
3985                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3986                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3987                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
3988
3989                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
3990
3991        return view
3992
3993    def RequestAccounts(self) -> dict:
3994        """
3995        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3996
3997        See also:
3998        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3999        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
4000        - `OverviewUserInfo()` method
4001
4002        :return: dict with raw data from server that contains accounts info. Example of dict:
4003                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4004                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4005                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4006                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4007        """
4008        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4009
4010        self.body = str({})
4011        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4012        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4013
4014        if self.moreDebug:
4015            uLogger.debug("Records about available accounts successfully received")
4016
4017        return rawAccounts
4018
4019    def RequestUserInfo(self) -> dict:
4020        """
4021        Method for requesting common user's information.
4022
4023        See also:
4024        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4025        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4026        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4027        - `OverviewUserInfo()` method
4028
4029        :return: dict with raw data from server that contains user's information. Example of dict:
4030                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4031                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4032        """
4033        uLogger.debug("Requesting common user's information. Wait, please...")
4034
4035        self.body = str({})
4036        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4037        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4038
4039        if self.moreDebug:
4040            uLogger.debug("Records about current user successfully received")
4041
4042        return rawUserInfo
4043
4044    def RequestMarginStatus(self, accountId: str = None) -> dict:
4045        """
4046        Method for requesting margin calculation for defined account ID.
4047
4048        See also:
4049        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4050        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4051        - `OverviewUserInfo()` method
4052
4053        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4054        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4055                 Example of responses:
4056                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4057                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4058                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4059                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4060                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4061                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4062        """
4063        if accountId is None or not accountId:
4064            if self.accountId is None or not self.accountId:
4065                uLogger.error("Variable `accountId` must be defined for using this method!")
4066                raise Exception("Account ID required")
4067
4068            else:
4069                accountId = self.accountId  # use `self.accountId` (main ID) by default
4070
4071        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4072
4073        self.body = str({"accountId": accountId})
4074        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4075        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4076
4077        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4078            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4079            rawMargin = {}
4080
4081        else:
4082            if self.moreDebug:
4083                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4084
4085        return rawMargin
4086
4087    def RequestTariffLimits(self) -> dict:
4088        """
4089        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4090
4091        See also:
4092        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4093        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4094        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4095        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4096        - `OverviewUserInfo()` method
4097
4098        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4099                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4100                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4101        """
4102        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4103
4104        self.body = str({})
4105        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4106        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4107
4108        if self.moreDebug:
4109            uLogger.debug("Records with limits of current tariff successfully received")
4110
4111        return rawTariffLimits
4112
4113    def RequestBondCoupons(self, iJSON: dict) -> dict:
4114        """
4115        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4116        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4117        All dates are in UTC timezone.
4118
4119        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4120        Documentation:
4121        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4122        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4123
4124        See also: `ExtendBondsData()`.
4125
4126        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4127                      If raw iJSON is not data of bond then server returns an error [400] with message:
4128                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4129        :return: dictionary with bond payment calendar. Response example
4130                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4131                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4132                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4133                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4134        """
4135        if iJSON["figi"] is None or not iJSON["figi"]:
4136            uLogger.error("FIGI must be defined for using this method!")
4137            raise Exception("FIGI required")
4138
4139        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4140        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4141
4142        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4143            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4144            self._figi,
4145            startDate,
4146            endDate,
4147        ))
4148
4149        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4150        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4151        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4152
4153        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4154            uLogger.warning("Instrument type is not bond!")
4155
4156        else:
4157            if self.moreDebug:
4158                uLogger.debug("Records about bond payment calendar successfully received")
4159
4160        return calendar
4161
4162    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4163        """
4164        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4165        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4166        coupon yields, current yields and some statistics etc.
4167
4168        WARNING! This is too long operation if a lot of bonds requested from broker server.
4169
4170        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4171
4172        :param instruments: list of strings with tickers or FIGIs.
4173        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4174                     for further used by data scientists or stock analytics.
4175        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4176                 In XLSX-file and Pandas DataFrame fields mean:
4177                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4178                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4179        """
4180        if instruments is None or not instruments:
4181            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4182            raise Exception("Ticker or FIGI required")
4183
4184        if isinstance(instruments, str):
4185            instruments = [instruments]
4186
4187        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4188
4189        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4190
4191        iCount = len(uniqueInstruments)
4192        tooLong = iCount >= 20
4193        if tooLong:
4194            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4195
4196        bonds = None
4197        for i, self._figi in enumerate(uniqueInstruments):
4198            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4199
4200            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4201                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4202                rawBond = self.SearchByFIGI(requestPrice=True)
4203
4204                # Widen raw data with UTC current time (iData["actualDateTime"]):
4205                actualDate = datetime.now(tzutc())
4206                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4207
4208                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4209                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4210
4211                # Replace some values with human-readable:
4212                iData["nominalCurrency"] = iData["nominal"]["currency"]
4213                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4214                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4215                iData["aciCurrency"] = iData["aciValue"]["currency"]
4216                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4217                iData["issueSize"] = int(iData["issueSize"])
4218                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4219                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4220                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4221                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4222                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4223                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4224                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4225                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4226                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4227                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4228
4229                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4230                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4231                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4232                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4233                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4234                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4235                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4236                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4237                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4238                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4239                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4240
4241                # Widen raw data with calendar data from `rawCalendar` values:
4242                calendarData = []
4243                if "events" in iData["rawCalendar"].keys():
4244                    for item in iData["rawCalendar"]["events"]:
4245                        calendarData.append({
4246                            "couponDate": item["couponDate"],
4247                            "couponNumber": int(item["couponNumber"]),
4248                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4249                            "payCurrency": item["payOneBond"]["currency"],
4250                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4251                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4252                            "couponStartDate": item["couponStartDate"],
4253                            "couponEndDate": item["couponEndDate"],
4254                            "couponPeriod": item["couponPeriod"],
4255                        })
4256
4257                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4258                    if "maturityDate" not in iData.keys():
4259                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4260
4261                # Widen raw data with Coupon Rate.
4262                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4263                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4264                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4265                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4266
4267                # Widen raw data with Yield to Maturity (YTM) on current date.
4268                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4269                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4270                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4271                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4272                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4273                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4274
4275                iData["calendar"] = calendarData  # adds calendar at the end
4276
4277                # Remove not used data:
4278                iData.pop("uid")
4279                iData.pop("positionUid")
4280                iData.pop("currentPrice")
4281                iData.pop("rawCalendar")
4282
4283                colNames = list(iData.keys())
4284                if bonds is None:
4285                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4286
4287                else:
4288                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4289
4290            else:
4291                uLogger.warning("Instrument is not a bond!")
4292
4293            processed = round(100 * (i + 1) / iCount, 1)
4294            if tooLong and processed % 5 == 0:
4295                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4296
4297            else:
4298                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4299
4300        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4301
4302        # Saving bonds from Pandas DataFrame to XLSX sheet:
4303        if xlsx and self.bondsXLSXFile:
4304            with pd.ExcelWriter(
4305                    path=self.bondsXLSXFile,
4306                    date_format=TKS_DATE_FORMAT,
4307                    datetime_format=TKS_DATE_TIME_FORMAT,
4308                    mode="w",
4309            ) as writer:
4310                bonds.to_excel(
4311                    writer,
4312                    sheet_name="Extended bonds data",
4313                    index=True,
4314                    encoding="UTF-8",
4315                    freeze_panes=(1, 1),
4316                )  # saving as XLSX-file with freeze first row and column as headers
4317
4318            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4319
4320        return bonds
4321
4322    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4323        """
4324        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4325
4326        WARNING! This is too long operation if a lot of bonds requested from broker server.
4327
4328        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4329
4330        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4331                        extended information about bonds: main info, current prices, bond payment calendar,
4332                        coupon yields, current yields and some statistics etc.
4333                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4334        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4335                     for further used by data scientists or stock analytics.
4336        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4337        """
4338        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4339            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4340
4341        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4342
4343        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4344        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4345        calendar = None
4346        for bond in extBonds.iterrows():
4347            for item in bond[1]["calendar"]:
4348                cData = {
4349                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4350                    "couponDate": item["couponDate"],
4351                    "figi": bond[1]["figi"],
4352                    "ticker": bond[1]["ticker"],
4353                    "name": bond[1]["name"],
4354                    "couponNumber": item["couponNumber"],
4355                    "payOneBond": item["payOneBond"],
4356                    "payCurrency": item["payCurrency"],
4357                    "couponType": item["couponType"],
4358                    "couponPeriod": item["couponPeriod"],
4359                    "fixDate": item["fixDate"],
4360                    "couponStartDate": item["couponStartDate"],
4361                    "couponEndDate": item["couponEndDate"],
4362                }
4363
4364                if calendar is None:
4365                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4366
4367                else:
4368                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4369
4370        if calendar is not None:
4371            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4372
4373            # Saving calendar from Pandas DataFrame to XLSX sheet:
4374            if xlsx:
4375                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4376
4377                with pd.ExcelWriter(
4378                        path=xlsxCalendarFile,
4379                        date_format=TKS_DATE_FORMAT,
4380                        datetime_format=TKS_DATE_TIME_FORMAT,
4381                        mode="w",
4382                ) as writer:
4383                    humanReadable = calendar.copy(deep=True)
4384                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4385                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4386                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4387                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4388                    humanReadable.columns = colNames  # human-readable column names
4389
4390                    humanReadable.to_excel(
4391                        writer,
4392                        sheet_name="Bond payments calendar",
4393                        index=False,
4394                        encoding="UTF-8",
4395                        freeze_panes=(1, 2),
4396                    )  # saving as XLSX-file with freeze first row and column as headers
4397
4398                    del humanReadable  # release df in memory
4399
4400                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4401
4402        return calendar
4403
4404    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4405        """
4406        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4407        Also, creates Markdown file with calendar data, `calendar.md` by default.
4408
4409        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4410
4411        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4412                        extended information about bonds: main info, current prices, bond payment calendar,
4413                        coupon yields, current yields and some statistics etc.
4414                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4415        :param show: if `True` then also printing bonds payment calendar to the console,
4416                     otherwise save to file `calendarFile` only. `False` by default.
4417        :return: multilines text in Markdown format with bonds payment calendar as a table.
4418        """
4419        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4420            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4421
4422        infoText = "# Bond payments calendar\n\n"
4423
4424        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4425
4426        if not (calendar is None or calendar.empty):
4427            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4428
4429            info = [
4430                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4431                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4432                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4433            ]
4434
4435            newMonth = False
4436            notOneBond = calendar["figi"].nunique() > 1
4437            for i, bond in enumerate(calendar.iterrows()):
4438                if newMonth and notOneBond:
4439                    info.append(splitLine)
4440
4441                info.append(
4442                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4443                        "  √" if bond[1]["paid"] else "  —",
4444                        bond[1]["couponDate"].split("T")[0],
4445                        bond[1]["figi"],
4446                        bond[1]["ticker"],
4447                        bond[1]["couponNumber"],
4448                        "{} {}".format(
4449                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4450                            bond[1]["payCurrency"],
4451                        ),
4452                        bond[1]["couponType"],
4453                        bond[1]["couponPeriod"],
4454                        bond[1]["fixDate"].split("T")[0],
4455                    )
4456                )
4457
4458                if i < len(calendar.values) - 1:
4459                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4460                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4461                    newMonth = False if curDate.month == nextDate.month else True
4462
4463                else:
4464                    newMonth = False
4465
4466            infoText += "".join(info)
4467
4468            if show:
4469                uLogger.info("{}".format(infoText))
4470
4471            if self.calendarFile is not None:
4472                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4473                    fH.write(infoText)
4474
4475                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4476
4477                if self.useHTMLReports:
4478                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4479                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4480                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4481
4482                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4483
4484        else:
4485            infoText += "No data\n"
4486
4487        return infoText
4488
4489    def OverviewAccounts(self, show: bool = False) -> dict:
4490        """
4491        Method for parsing and show simple table with all available user accounts.
4492
4493        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4494
4495        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4496        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4497                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4498                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4499                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4500                                                        "closed": "—", "access": "Full access" }, ...}}`
4501        """
4502        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4503
4504        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4505        accounts = {
4506            item["id"]: {
4507                "type": TKS_ACCOUNT_TYPES[item["type"]],
4508                "name": item["name"],
4509                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4510                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4511                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4512                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4513            } for item in rawAccounts["accounts"]
4514        }
4515
4516        # Raw and parsed data with some fields replaced in "stat" section:
4517        view = {
4518            "rawAccounts": rawAccounts,
4519            "stat": accounts,
4520        }
4521
4522        # --- Prepare simple text table with only accounts data in human-readable format:
4523        if show:
4524            info = [
4525                "# User accounts\n\n",
4526                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4527                "| Account ID   | Type                      | Status                    | Name                           |\n",
4528                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4529            ]
4530
4531            for account in view["stat"].keys():
4532                info.extend([
4533                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4534                        account,
4535                        view["stat"][account]["type"],
4536                        view["stat"][account]["status"],
4537                        view["stat"][account]["name"],
4538                    )
4539                ])
4540
4541            infoText = "".join(info)
4542
4543            uLogger.info(infoText)
4544
4545            if self.userAccountsFile:
4546                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4547                    fH.write(infoText)
4548
4549                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4550
4551                if self.useHTMLReports:
4552                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4553                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4554                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4555
4556                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4557
4558        return view
4559
4560    def OverviewUserInfo(self, show: bool = False) -> dict:
4561        """
4562        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4563
4564        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4565
4566        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4567        :return: dict with raw parsed data from server and some calculated statistics about it.
4568        """
4569        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4570        tmpTicker = self._ticker
4571        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4572        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4573        self._ticker = tmpTicker
4574
4575        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4576        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4577        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4578        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4579        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4580        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4581
4582        # This is dict with parsed common user data:
4583        userInfo = {
4584            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4585            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4586            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4587            "tariff": rawUserInfo["tariff"],
4588        }
4589
4590        # This is an array of dict with parsed margin statuses for every account IDs:
4591        margins = {}
4592        for accountId in accounts.keys():
4593            if rawMargins[accountId]:
4594                margins[accountId] = {
4595                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4596                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4597                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4598                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4599                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4600                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4601                    "missing": missing["volume"],
4602                }
4603
4604            else:
4605                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4606
4607        unary = {}  # unary-connection limits
4608        for item in rawTariffLimits["unaryLimits"]:
4609            if item["limitPerMinute"] in unary.keys():
4610                unary[item["limitPerMinute"]].extend(item["methods"])
4611
4612            else:
4613                unary[item["limitPerMinute"]] = item["methods"]
4614
4615        stream = {}  # stream-connection limits
4616        for item in rawTariffLimits["streamLimits"]:
4617            if item["limit"] in stream.keys():
4618                stream[item["limit"]].extend(item["streams"])
4619
4620            else:
4621                stream[item["limit"]] = item["streams"]
4622
4623        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4624        limits = {
4625            "unary": unary,
4626            "stream": stream,
4627        }
4628
4629        # Raw and parsed data as an output result:
4630        view = {
4631            "rawUserInfo": rawUserInfo,
4632            "rawAccounts": rawAccounts,
4633            "rawMargins": rawMargins,
4634            "rawTariffLimits": rawTariffLimits,
4635            "stat": {
4636                "overview": overview,
4637                "userInfo": userInfo,
4638                "accounts": accounts,
4639                "margins": margins,
4640                "limits": limits,
4641            },
4642        }
4643
4644        # --- Prepare text table with user information in human-readable format:
4645        if show:
4646            info = [
4647                "# Full user information\n\n",
4648                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4649                "## Common information\n\n",
4650                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4651                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4652                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4653                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4654                "\n## User accounts\n\n",
4655            ]
4656
4657            for account in view["stat"]["accounts"].keys():
4658                info.extend([
4659                    "### ID: [{}]\n\n".format(account),
4660                    "| Parameters           | Values                                                       |\n",
4661                    "|----------------------|--------------------------------------------------------------|\n",
4662                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4663                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4664                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4665                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4666                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4667                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4668                ])
4669
4670                if margins[account]:
4671                    info.extend([
4672                        "| Margin status:       | Enabled                                                      |\n",
4673                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4674                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4675                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4676                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4677                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4678                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4679                    ])
4680
4681                else:
4682                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4683
4684            info.extend([
4685                "\n## Current user tariff limits\n",
4686                "\n### See also\n",
4687                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4688                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4689                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4690                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4691                "\n### Unary limits\n",
4692            ])
4693
4694            if unary:
4695                for key, values in sorted(unary.items()):
4696                    info.append("\n* Max requests per minute: {}\n".format(key))
4697
4698                    for value in values:
4699                        info.append("  - {}\n".format(value))
4700
4701            else:
4702                info.append("\nNot available\n")
4703
4704            info.append("\n### Stream limits\n")
4705
4706            if stream:
4707                for key, values in sorted(stream.items()):
4708                    info.append("\n* Max stream connections: {}\n".format(key))
4709
4710                    for value in values:
4711                        info.append("  - {}\n".format(value))
4712
4713            else:
4714                info.append("\nNot available\n")
4715
4716            infoText = "".join(info)
4717
4718            uLogger.info(infoText)
4719
4720            if self.userInfoFile:
4721                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4722                    fH.write(infoText)
4723
4724                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4725
4726                if self.useHTMLReports:
4727                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4728                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4729                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4730
4731                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4732
4733        return view

This class implements methods to work with Tinkoff broker server.

Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/

About token: https://tinkoff.github.io/investAPI/token/

TinkoffBrokerServer( token: str, accountId: str = None, useCache: bool = True, defaultCache: str = 'dump.json')
 86    def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None:
 87        """
 88        Main class init.
 89
 90        :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`.
 91        :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
 92                          Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.
 93        :param useCache: use default cache file with raw data to use instead of `iList`.
 94                         True by default. Cache is auto-update if new day has come.
 95                         If you don't want to use cache and always updates raw data then set `useCache=False`.
 96        :param defaultCache: path to default cache file. `dump.json` by default.
 97        """
 98        if token is None or not token:
 99            try:
100                self.token = r"{}".format(os.environ["TKS_API_TOKEN"])
101                uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/")
102
103            except KeyError:
104                uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/")
105                raise Exception("Token required")
106
107        else:
108            self.token = token  # highly priority than environment variable 'TKS_API_TOKEN'
109            uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`")
110
111        if accountId is None or not accountId:
112            try:
113                self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"])
114                uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId))
115
116            except KeyError:
117                uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).")
118
119        else:
120            self.accountId = accountId  # highly priority than environment variable 'TKS_ACCOUNT_ID'
121            uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId))
122
123        self.version = __version__  # duplicate here used TKSBrokerAPI main version
124        """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
125
126        Latest version: https://pypi.org/project/tksbrokerapi/
127        """
128
129        self.__lock = Lock()  # initialize multiprocessing mutex lock
130
131        self.aliases = TKS_TICKER_ALIASES
132        """Some aliases instead official tickers.
133
134        See also: `TKSEnums.TKS_TICKER_ALIASES`
135        """
136
137        self.aliasesKeys = self.aliases.keys()  # re-calc only first time at class init
138
139        self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED  # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there
140
141        self._ticker = ""
142        """String with ticker, e.g. `GOOGL`. Tickers may be upper case only.
143
144        Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc.
145        More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`.
146
147        See also: `SearchByTicker()`, `SearchInstruments()`.
148        """
149
150        self._figi = ""
151        """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. FIGIs may be upper case only.
152
153        See also: `SearchByFIGI()`, `SearchInstruments()`.
154        """
155
156        self.depth = 1
157        """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI.
158
159        See also: `GetCurrentPrices()`.
160        """
161
162        self.server = r"https://invest-public-api.tinkoff.ru/rest"
163        """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
164
165        See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`.
166        """
167
168        uLogger.debug("Broker API server: {}".format(self.server))
169
170        self.timeout = 15
171        """Server operations timeout in seconds. Default: `15`.
172
173        See also: `SendAPIRequest()`.
174        """
175
176        self.headers = {
177            "Content-Type": "application/json",
178            "accept": "application/json",
179            "Authorization": "Bearer {}".format(self.token),
180            "x-app-name": "Tim55667757.TKSBrokerAPI",
181        }
182        """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`.
183
184        See also: `SendAPIRequest()`.
185        """
186
187        self.body = None
188        """Request body which send to broker server. Default: `None`.
189
190        See also: `SendAPIRequest()`.
191        """
192
193        self.moreDebug = False
194        """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default."""
195
196        self.useHTMLReports = False
197        """
198        If `True` then TKSBrokerAPI generate also HTML reports from Markdown. `False` by default.
199        
200        See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.
201        """
202
203        self.historyFile = None
204        """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame.
205
206        See also: `History()`.
207        """
208
209        self.htmlHistoryFile = "index.html"
210        """Full path to the html file where rendered candles chart stored. Default: `index.html`.
211
212        See also: `ShowHistoryChart()`.
213        """
214
215        self.instrumentsFile = "instruments.md"
216        """Filename where full available to user instruments list will be saved. Default: `instruments.md`.
217
218        See also: `ShowInstrumentsInfo()`.
219        """
220
221        self.searchResultsFile = "search-results.md"
222        """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`.
223
224        See also: `SearchInstruments()`.
225        """
226
227        self.pricesFile = "prices.md"
228        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
229
230        See also: `GetListOfPrices()`.
231        """
232
233        self.infoFile = "info.md"
234        """Filename where prices of selected instruments will be saved. Default: `prices.md`.
235
236        See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`.
237        """
238
239        self.bondsXLSXFile = "ext-bonds.xlsx"
240        """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 
241        bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`.
242
243        See also: `ExtendBondsData()`.
244        """
245
246        self.calendarFile = "calendar.md"
247        """Filename where bonds payment calendar will be saved. Default: `calendar.md`.
248        
249        Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`.
250
251        See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`.
252        """
253
254        self.overviewFile = "overview.md"
255        """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`.
256
257        See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`.
258        """
259
260        self.overviewDigestFile = "overview-digest.md"
261        """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`.
262
263        See also: `Overview()` with parameter `details="digest"`.
264        """
265
266        self.overviewPositionsFile = "overview-positions.md"
267        """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`.
268
269        See also: `Overview()` with parameter `details="positions"`.
270        """
271
272        self.overviewOrdersFile = "overview-orders.md"
273        """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`.
274
275        See also: `Overview()` with parameter `details="orders"`.
276        """
277
278        self.overviewAnalyticsFile = "overview-analytics.md"
279        """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`.
280
281        See also: `Overview()` with parameter `details="analytics"`.
282        """
283
284        self.overviewBondsCalendarFile = "overview-calendar.md"
285        """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`.
286
287        See also: `Overview()` with parameter `details="calendar"`.
288        """
289
290        self.reportFile = "deals.md"
291        """Filename where history of deals and trade statistics will be saved. Default: `deals.md`.
292
293        See also: `Deals()`.
294        """
295
296        self.withdrawalLimitsFile = "limits.md"
297        """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`.
298
299        See also: `OverviewLimits()` and `RequestLimits()`.
300        """
301
302        self.userInfoFile = "user-info.md"
303        """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`.
304
305        See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`.
306        """
307
308        self.userAccountsFile = "accounts.md"
309        """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`.
310
311        See also: `OverviewAccounts()`, `RequestAccounts()`.
312        """
313
314        self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache
315        """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`.
316
317        Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`.
318
319        See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`.
320        """
321
322        self.iList = None  # init iList for raw instruments data
323        """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`.
324        
325        See also: `Listing()`, `DumpInstruments()`.
326        """
327
328        # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server:
329        if useCache:
330            if os.path.exists(self.iListDumpFile):
331                dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc())  # dump modification date and time
332                curTime = datetime.now(tzutc())
333
334                if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year):
335                    uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
336
337                    self.DumpInstruments(forceUpdate=True)  # updating self.iList and dump file
338
339                else:
340                    self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8"))  # load iList from dump
341
342                    uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format(
343                        os.path.abspath(self.iListDumpFile),
344                        dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT),
345                    ))
346
347            else:
348                uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...")
349                self.DumpInstruments(forceUpdate=True)  # updating self.iList and creating default dump file
350
351        else:
352            self.iList = self.Listing()  # request new raw instruments data from broker server
353            self.DumpInstruments(forceUpdate=False)  # save raw instrument's data to default dump file `iListDumpFile`
354
355        self.priceModel = PriceGenerator()  # init PriceGenerator object to work with candles data
356        """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
357
358        See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
359        """

Main class init.

Parameters
  • token: Bearer token for Tinkoff Invest API. It can be set from environment variable TKS_API_TOKEN.
  • accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. Also, this variable can be set from environment variable TKS_ACCOUNT_ID.
  • useCache: use default cache file with raw data to use instead of iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then set useCache=False.
  • defaultCache: path to default cache file. dump.json by default.
version

Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.

Latest version: https://pypi.org/project/tksbrokerapi/

aliases

Some aliases instead official tickers.

See also: TKSEnums.TKS_TICKER_ALIASES

depth

Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.

See also: GetCurrentPrices().

server

Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest

See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().

timeout

Server operations timeout in seconds. Default: 15.

See also: SendAPIRequest().

headers

Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.

See also: SendAPIRequest().

body

Request body which send to broker server. Default: None.

See also: SendAPIRequest().

moreDebug

Enables more debug information in this class, such as net request and response headers in all methods. False by default.

useHTMLReports

If True then TKSBrokerAPI generate also HTML reports from Markdown. False by default.

See also: Mako Templates for Python (https://www.makotemplates.org/). Mako is a template library provides simple syntax and maximum performance.

historyFile

Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.

See also: History().

htmlHistoryFile

Full path to the html file where rendered candles chart stored. Default: index.html.

See also: ShowHistoryChart().

instrumentsFile

Filename where full available to user instruments list will be saved. Default: instruments.md.

See also: ShowInstrumentsInfo().

searchResultsFile

Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.

See also: SearchInstruments().

pricesFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: GetListOfPrices().

infoFile

Filename where prices of selected instruments will be saved. Default: prices.md.

See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().

bondsXLSXFile

Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.

See also: ExtendBondsData().

calendarFile

Filename where bonds payment calendar will be saved. Default: calendar.md.

Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.

See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().

overviewFile

Filename where current portfolio, open trades and orders will be saved. Default: overview.md.

See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().

overviewDigestFile

Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.

See also: Overview() with parameter details="digest".

overviewPositionsFile

Filename where only open positions, without everything else will be saved. Default: overview-positions.md.

See also: Overview() with parameter details="positions".

overviewOrdersFile

Filename where open limits and stop orders will be saved. Default: overview-orders.md.

See also: Overview() with parameter details="orders".

overviewAnalyticsFile

Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.

See also: Overview() with parameter details="analytics".

overviewBondsCalendarFile

Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.

See also: Overview() with parameter details="calendar".

reportFile

Filename where history of deals and trade statistics will be saved. Default: deals.md.

See also: Deals().

withdrawalLimitsFile

Filename where table of funds available for withdrawal will be saved. Default: limits.md.

See also: OverviewLimits() and RequestLimits().

userInfoFile

Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.

See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().

userAccountsFile

Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.

See also: OverviewAccounts(), RequestAccounts().

iListDumpFile

Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.

Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.

See also: DumpInstruments() and DumpInstrumentsAsXLSX().

iList

Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.

See also: Listing(), DumpInstruments().

priceModel

PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.

See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator

ticker: str

Setter for string with ticker, e.g. GOOGL. Tickers may be upper case only.

Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.

See also: SearchByTicker(), SearchInstruments().

figi: str

Setter for string with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6. FIGIs may be upper case only.

See also: SearchByFIGI(), SearchInstruments().

def SendAPIRequest( self, url: str, reqType: str = 'GET', retry: int = 3, pause: int = 5) -> dict:
419    def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict:
420        """
421        Send GET or POST request to broker server and receive JSON object.
422
423        self.header: must be defining with dictionary of headers.
424        self.body: if define then used as request body. None by default.
425        self.timeout: global request timeout, 15 seconds by default.
426        :param url: url with REST request.
427        :param reqType: send "GET" or "POST" request. "GET" by default.
428        :param retry: how many times retry after first request if an 5xx server errors occurred.
429        :param pause: sleep time in seconds between retries.
430        :return: response JSON (dictionary) from broker.
431        """
432        if reqType.upper() not in ("GET", "POST"):
433            uLogger.error("You can define request type: `GET` or `POST`!")
434            raise Exception("Incorrect value")
435
436        if self.moreDebug:
437            uLogger.debug("Request parameters:")
438            uLogger.debug("    - REST API URL: {}".format(url))
439            uLogger.debug("    - request type: {}".format(reqType))
440            uLogger.debug("    - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***")))
441            uLogger.debug("    - body:\n{}".format(self.body))
442
443        # fast hack to avoid all operations with some tickers/FIGI
444        responseJSON = {}
445        oK = True
446        for item in self.exclude:
447            if item in url:
448                if self.moreDebug:
449                    uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude)))
450
451                oK = False
452                break
453
454        if oK:
455            with self.__lock:  # acquire the mutex lock
456                counter = 0
457                response = None
458                errMsg = ""
459
460                while not response and counter <= retry:
461                    if reqType == "GET":
462                        response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout)
463
464                    if reqType == "POST":
465                        response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout)
466
467                    if self.moreDebug:
468                        uLogger.debug("Response:")
469                        uLogger.debug("    - status code: {}".format(response.status_code))
470                        uLogger.debug("    - reason: {}".format(response.reason))
471                        uLogger.debug("    - body length: {}".format(len(response.text)))
472                        uLogger.debug("    - headers:\n{}".format(response.headers))
473
474                    # Server returns some headers:
475                    # - `x-ratelimit-limit` — shows the settings of the current user limit for this method.
476                    # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute.
477                    # - `x-ratelimit-reset` — time in seconds before resetting the request counter.
478                    # See: https://tinkoff.github.io/investAPI/grpc/#kreya
479                    if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0":
480                        rateLimitWait = int(response.headers["x-ratelimit-reset"])
481                        uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait))
482                        sleep(rateLimitWait)
483
484                    # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
485                    if 400 <= response.status_code < 500:
486                        msg = "status code: [{}], response body: {}".format(response.status_code, response.text)
487                        uLogger.debug("    - not oK, but do not retry for 4xx errors, {}".format(msg))
488
489                        if "code" in response.text and "message" in response.text:
490                            msgDict = self._ParseJSON(rawData=response.text)
491                            uLogger.warning("HTTP-status code [{}], server message: {}".format(response.status_code, msgDict["message"]))
492
493                        counter = retry + 1  # do not retry for 4xx errors
494
495                    if 500 <= response.status_code < 600:
496                        errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text)
497                        uLogger.debug("    - not oK, {}".format(errMsg))
498
499                        if "code" in response.text and "message" in response.text:
500                            errMsgDict = self._ParseJSON(rawData=response.text)
501                            uLogger.warning("HTTP-status code [{}], error message: {}".format(response.status_code, errMsgDict["message"]))
502
503                        counter += 1
504
505                        if counter <= retry:
506                            uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause))
507                            sleep(pause)
508
509                responseJSON = self._ParseJSON(rawData=response.text)
510
511                if errMsg:
512                    uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/")
513                    uLogger.error("    - not oK, {}".format(errMsg))
514
515        return responseJSON

Send GET or POST request to broker server and receive JSON object.

self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.

Parameters
  • url: url with REST request.
  • reqType: send "GET" or "POST" request. "GET" by default.
  • retry: how many times retry after first request if an 5xx server errors occurred.
  • pause: sleep time in seconds between retries.
Returns

response JSON (dictionary) from broker.

def Listing(self) -> dict:
548    def Listing(self) -> dict:
549        """
550        Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
551
552        :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
553        """
554        uLogger.debug("Requesting all available instruments for current account. Wait, please...")
555        uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES))
556
557        # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService
558        # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list.
559        iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS]
560
561        poolUpdater = ThreadPool(processes=CPU_USAGES)  # create pool for update instruments in parallel mode
562        listing = poolUpdater.map(self._IWrapper, iParams)  # execute update operations
563        poolUpdater.close()  # close the thread pool
564        poolUpdater.join()  # wait a moment until all data returns from threads
565
566        # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures.
567        # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method
568        iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing}
569
570        # calculate minimum price increment (step) for all instruments and set up instrument's type:
571        for iType in iList.keys():
572            for ticker in iList[iType]:
573                iList[iType][ticker]["type"] = iType
574
575                if "minPriceIncrement" in iList[iType][ticker].keys():
576                    iList[iType][ticker]["step"] = NanoToFloat(
577                        iList[iType][ticker]["minPriceIncrement"]["units"],
578                        iList[iType][ticker]["minPriceIncrement"]["nano"],
579                    )
580
581                else:
582                    iList[iType][ticker]["step"] = 0  # hack to avoid empty value in some instruments, e.g. futures
583
584        return iList

Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.

Returns

Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.

def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
586    def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None:
587        """
588        Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
589
590        See also: `DumpInstruments()`, `Listing()`.
591
592        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
593                            otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) .
594        """
595        if self.iListDumpFile is None or not self.iListDumpFile:
596            uLogger.error("Output name of dump file must be defined!")
597            raise Exception("Filename required")
598
599        if not self.iList or forceUpdate:
600            self.iList = self.Listing()
601
602        xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx"
603
604        # Save as XLSX with separated sheets for every type of instruments:
605        with pd.ExcelWriter(
606                path=xlsxDumpFile,
607                date_format=TKS_DATE_FORMAT,
608                datetime_format=TKS_DATE_TIME_FORMAT,
609                mode="w",
610        ) as writer:
611            for iType in TKS_INSTRUMENTS:
612                df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index")  # generate pandas object from self.iList dictionary
613                df = df[sorted(df)]  # sorted by column names
614                df = df.applymap(
615                    lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item,
616                    na_action="ignore",
617                )  # converting numbers from nano-type to float in every cell
618                df.to_excel(
619                    writer,
620                    sheet_name=iType,
621                    encoding="UTF-8",
622                    freeze_panes=(1, 1),
623                )  # saving as XLSX-file with freeze first row and column as headers
624
625        uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))

Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.

See also: DumpInstruments(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as XLSX-file (default: dump.xlsx) .
def DumpInstruments(self, forceUpdate: bool = True) -> str:
627    def DumpInstruments(self, forceUpdate: bool = True) -> str:
628        """
629        Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
630        using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file.
631
632        See also: `DumpInstrumentsAsXLSX()`, `Listing()`.
633
634        :param forceUpdate: if `True` then at first updates data with `Listing()` method,
635                            otherwise just saves exist `iList` as JSON-file (default: `dump.json`).
636        :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file.
637        """
638        if self.iListDumpFile is None or not self.iListDumpFile:
639            uLogger.error("Output name of dump file must be defined!")
640            raise Exception("Filename required")
641
642        if not self.iList or forceUpdate:
643            self.iList = self.Listing()
644
645        jsonDump = json.dumps(self.iList, indent=4, sort_keys=False)  # create JSON object as string
646        with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH:
647            fH.write(jsonDump)
648
649        uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile)))
650
651        return jsonDump

Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server using Listing() method. If iListDumpFile string is not empty then also save information to this file.

See also: DumpInstrumentsAsXLSX(), Listing().

Parameters
  • forceUpdate: if True then at first updates data with Listing() method, otherwise just saves exist iList as JSON-file (default: dump.json).
Returns

serialized JSON formatted str with full data of instruments, also saved to the --output JSON-file.

def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
653    def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str:
654        """
655        Show information about one instrument defined by json data and prints it in Markdown format.
656
657        See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`.
658
659        :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self._ticker]`
660        :param show: if `True` then also printing information about instrument and its current price.
661        :return: multilines text in Markdown format with information about one instrument.
662        """
663        splitLine = "|                                                             |                                                        |\n"
664        infoText = ""
665
666        if iJSON is not None and iJSON and isinstance(iJSON, dict):
667            info = [
668                "# Main information\n\n",
669                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
670                "| Parameters                                                  | Values                                                 |\n",
671                "|-------------------------------------------------------------|--------------------------------------------------------|\n",
672                "| Ticker:                                                     | {:<54} |\n".format(iJSON["ticker"]),
673                "| Full name:                                                  | {:<54} |\n".format(iJSON["name"]),
674            ]
675
676            if "sector" in iJSON.keys() and iJSON["sector"]:
677                info.append("| Sector:                                                     | {:<54} |\n".format(iJSON["sector"]))
678
679            if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] and "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"]:
680                info.append("| Country of instrument:                                      | {:<54} |\n".format("({}) {}".format(iJSON["countryOfRisk"], iJSON["countryOfRiskName"])))
681
682            info.extend([
683                splitLine,
684                "| FIGI (Financial Instrument Global Identifier):              | {:<54} |\n".format(iJSON["figi"]),
685                "| Real exchange [Exchange section]:                           | {:<54} |\n".format("{} [{}]".format(TKS_REAL_EXCHANGES[iJSON["realExchange"]], iJSON["exchange"])),
686            ])
687
688            if "isin" in iJSON.keys() and iJSON["isin"]:
689                info.append("| ISIN (International Securities Identification Number):      | {:<54} |\n".format(iJSON["isin"]))
690
691            if "classCode" in iJSON.keys():
692                info.append("| Class Code (exchange section where instrument is traded):   | {:<54} |\n".format(iJSON["classCode"]))
693
694            info.extend([
695                splitLine,
696                "| Current broker security trading status:                     | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]),
697                splitLine,
698                "| Buy operations allowed:                                     | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"),
699                "| Sale operations allowed:                                    | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"),
700                "| Short positions allowed:                                    | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"),
701            ])
702
703            if iJSON["figi"]:
704                self._figi = iJSON["figi"]
705                iJSON = iJSON | self.RequestTradingStatus()
706
707                info.extend([
708                    splitLine,
709                    "| Limit orders allowed:                                       | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"),
710                    "| Market orders allowed:                                      | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"),
711                    "| API trade allowed:                                          | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"),
712                ])
713
714            info.append(splitLine)
715
716            if "type" in iJSON.keys() and iJSON["type"]:
717                info.append("| Type of the instrument:                                     | {:<54} |\n".format(iJSON["type"]))
718
719                if "shareType" in iJSON.keys() and iJSON["shareType"]:
720                    info.append("| Share type:                                                 | {:<54} |\n".format(TKS_SHARE_TYPES[iJSON["shareType"]]))
721
722            if "futuresType" in iJSON.keys() and iJSON["futuresType"]:
723                info.append("| Futures type:                                               | {:<54} |\n".format(iJSON["futuresType"]))
724
725            if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]:
726                info.append("| IPO date:                                                   | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", "")))
727
728            if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]:
729                info.append("| Released date:                                              | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", "")))
730
731            if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]:
732                info.append("| Rebalancing frequency:                                      | {:<54} |\n".format(iJSON["rebalancingFreq"]))
733
734            if "focusType" in iJSON.keys() and iJSON["focusType"]:
735                info.append("| Focusing type:                                              | {:<54} |\n".format(iJSON["focusType"]))
736
737            if "assetType" in iJSON.keys() and iJSON["assetType"]:
738                info.append("| Asset type:                                                 | {:<54} |\n".format(iJSON["assetType"]))
739
740            if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]:
741                info.append("| Basic asset:                                                | {:<54} |\n".format(iJSON["basicAsset"]))
742
743            if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]:
744                info.append("| Basic asset size:                                           | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"]))))
745
746            if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]:
747                info.append("| ISO currency name:                                          | {:<54} |\n".format(iJSON["isoCurrencyName"]))
748
749            if "currency" in iJSON.keys():
750                info.append("| Payment currency:                                           | {:<54} |\n".format(iJSON["currency"]))
751
752            if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys():
753                info.append("| Nominal currency:                                           | {:<54} |\n".format(iJSON["nominal"]["currency"]))
754
755            if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]:
756                info.append("| First trade date:                                           | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", "")))
757
758            if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]:
759                info.append("| Last trade date:                                            | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", "")))
760
761            if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]:
762                info.append("| Date of expiration:                                         | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", "")))
763
764            if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]:
765                info.append("| State registration date:                                    | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", "")))
766
767            if "placementDate" in iJSON.keys() and iJSON["placementDate"]:
768                info.append("| Placement date:                                             | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", "")))
769
770            if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]:
771                info.append("| Maturity date:                                              | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", "")))
772
773            if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]:
774                info.append("| Perpetual bond:                                             | Yes                                                    |\n")
775
776            if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]:
777                info.append("| Over-the-counter (OTC) securities:                          | Yes                                                    |\n")
778
779            iExt = None
780            if iJSON["type"] == "Bonds":
781                info.extend([
782                    splitLine,
783                    "| Bond issue (size / plan):                                   | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])),
784                    "| Nominal price (100%):                                       | {:<54} |\n".format("{} {}".format(
785                        "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."),
786                        iJSON["nominal"]["currency"],
787                    )),
788                ])
789
790                if "floatingCouponFlag" in iJSON.keys():
791                    info.append("| Floating coupon:                                            | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No"))
792
793                if "amortizationFlag" in iJSON.keys():
794                    info.append("| Amortization:                                               | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No"))
795
796                info.append(splitLine)
797
798                if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]:
799                    info.append("| Number of coupon payments per year:                         | {:<54} |\n".format(iJSON["couponQuantityPerYear"]))
800
801                if iJSON["figi"]:
802                    iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False)  # extended bonds data
803
804                    info.extend([
805                        "| Days last to maturity date:                                 | {:<54} |\n".format(iExt["daysToMaturity"][0]),
806                        "| Coupons yield (average coupon daily yield * 365):           | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])),
807                        "| Current price yield (average daily yield * 365):            | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])),
808                    ])
809
810                if "aciValue" in iJSON.keys() and iJSON["aciValue"]:
811                    info.append("| Current accumulated coupon income (ACI):                    | {:<54} |\n".format("{:.2f} {}".format(
812                        NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]),
813                        iJSON["aciValue"]["currency"]
814                    )))
815
816            if "currentPrice" in iJSON.keys():
817                info.append(splitLine)
818
819                currency = iJSON["currency"] if "currency" in iJSON.keys() else ""  # nominal currency for bonds, otherwise currency of instrument
820                aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else ""  # payment currency
821
822                bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0  # previous close price of bond
823                bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0  # last price of bond
824                bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0  # max price of bond
825                bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0  # min price of bond
826                bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0  # delta between last deal price and last close
827
828                curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0
829                curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0
830
831                info.extend([
832                    "| Previous close price of the instrument:                     | {:<54} |\n".format("{}{}".format(
833                        "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A",
834                        "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
835                    )),
836                    "| Last deal price of the instrument:                          | {:<54} |\n".format("{}{}".format(
837                        "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A",
838                        "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency),
839                    )),
840                    "| Changes between last deal price and last close              | {:<54} |\n".format(
841                        "{:.2f}%{}".format(
842                            iJSON["currentPrice"]["changes"],
843                            " ({}{:.2f} {})".format(
844                                "+" if bondChangesDelta > 0 else "",
845                                bondChangesDelta,
846                                aciCurrency
847                            ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format(
848                                "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "",
849                                iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"],
850                                currency
851                            ),
852                        )
853                    ),
854                    "| Current limit price, min / max:                             | {:<54} |\n".format("{}{} / {}{}{}".format(
855                        "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A",
856                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
857                        "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A",
858                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
859                        " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else ""
860                    )),
861                    "| Actual price, sell / buy:                                   | {:<54} |\n".format("{}{} / {}{}{}".format(
862                        "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A",
863                        "%" if iJSON["type"] == "Bonds" else " {}".format(currency),
864                        "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A",
865                        "%" if iJSON["type"] == "Bonds" else" {}".format(currency),
866                        " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else ""
867                    )),
868                ])
869
870            if "lot" in iJSON.keys():
871                info.append("| Minimum lot to buy:                                         | {:<54} |\n".format(iJSON["lot"]))
872
873            if "step" in iJSON.keys() and iJSON["step"] != 0:
874                info.append("| Minimum price increment (step):                             | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else "")))
875
876            # Add bond payment calendar:
877            if iJSON["type"] == "Bonds":
878                strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False)   # bond payment calendar
879                info.extend(["\n#", strCalendar])
880
881            infoText += "".join(info)
882
883            if show:
884                uLogger.info("{}".format(infoText))
885
886            else:
887                uLogger.debug("{}".format(infoText))
888
889            if self.infoFile is not None:
890                with open(self.infoFile, "w", encoding="UTF-8") as fH:
891                    fH.write(infoText)
892
893                uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile)))
894
895                if self.useHTMLReports:
896                    htmlFilePath = self.infoFile.replace(".md", ".html") if self.infoFile.endswith(".md") else self.infoFile + ".html"
897                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
898                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Main information", commonCSS=COMMON_CSS, markdown=infoText))
899
900                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
901
902        return infoText

Show information about one instrument defined by json data and prints it in Markdown format.

See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().

Parameters
  • iJSON: json data of instrument, example: iJSON = self.iList["Shares"][self._ticker]
  • show: if True then also printing information about instrument and its current price.
Returns

multilines text in Markdown format with information about one instrument.

def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
904    def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict:
905        """
906        Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined!
907
908        :param requestPrice: if `False` then do not request current price of instrument (because this is long operation).
909        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
910        :return: JSON formatted data with information about instrument.
911        """
912        tickerJSON = {}
913        if self.moreDebug:
914            uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self._ticker))
915
916        if not self._ticker:
917            uLogger.warning("self._ticker variable is not be empty!")
918
919        else:
920            if self._ticker in TKS_TICKERS_OR_FIGI_EXCLUDED:
921                uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self._ticker))
922                raise Exception("Instrument not allowed")
923
924            if not self.iList:
925                self.iList = self.Listing()
926
927            if self._ticker in self.iList["Shares"].keys():
928                tickerJSON = self.iList["Shares"][self._ticker]
929                if self.moreDebug:
930                    uLogger.debug("Ticker [{}] found in shares list".format(self._ticker))
931
932            elif self._ticker in self.iList["Currencies"].keys():
933                tickerJSON = self.iList["Currencies"][self._ticker]
934                if self.moreDebug:
935                    uLogger.debug("Ticker [{}] found in currencies list".format(self._ticker))
936
937            elif self._ticker in self.iList["Bonds"].keys():
938                tickerJSON = self.iList["Bonds"][self._ticker]
939                if self.moreDebug:
940                    uLogger.debug("Ticker [{}] found in bonds list".format(self._ticker))
941
942            elif self._ticker in self.iList["Etfs"].keys():
943                tickerJSON = self.iList["Etfs"][self._ticker]
944                if self.moreDebug:
945                    uLogger.debug("Ticker [{}] found in etfs list".format(self._ticker))
946
947            elif self._ticker in self.iList["Futures"].keys():
948                tickerJSON = self.iList["Futures"][self._ticker]
949                if self.moreDebug:
950                    uLogger.debug("Ticker [{}] found in futures list".format(self._ticker))
951
952        if tickerJSON:
953            self._figi = tickerJSON["figi"]
954
955            if requestPrice:
956                tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False)
957
958                if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None:
959                    tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"]
960
961                else:
962                    tickerJSON["currentPrice"]["changes"] = 0
963
964            if show:
965                self.ShowInstrumentInfo(iJSON=tickerJSON, show=True)  # print info as Markdown text
966
967        else:
968            if show:
969                uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self._ticker))
970
971        return tickerJSON

Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (because this is long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 973    def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict:
 974        """
 975        Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined!
 976
 977        :param requestPrice: if `False` then do not request current price of instrument (it's long operation).
 978        :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console.
 979        :return: JSON formatted data with information about instrument.
 980        """
 981        figiJSON = {}
 982        if self.moreDebug:
 983            uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self._figi))
 984
 985        if not self._figi:
 986            uLogger.warning("self._figi variable is not be empty!")
 987
 988        else:
 989            if self._figi in TKS_TICKERS_OR_FIGI_EXCLUDED:
 990                uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self._figi))
 991                raise Exception("Instrument not allowed")
 992
 993            if not self.iList:
 994                self.iList = self.Listing()
 995
 996            for item in self.iList["Shares"].keys():
 997                if self._figi == self.iList["Shares"][item]["figi"]:
 998                    figiJSON = self.iList["Shares"][item]
 999
1000                    if self.moreDebug:
1001                        uLogger.debug("FIGI [{}] found in shares list".format(self._figi))
1002
1003                    break
1004
1005            if not figiJSON:
1006                for item in self.iList["Currencies"].keys():
1007                    if self._figi == self.iList["Currencies"][item]["figi"]:
1008                        figiJSON = self.iList["Currencies"][item]
1009
1010                        if self.moreDebug:
1011                            uLogger.debug("FIGI [{}] found in currencies list".format(self._figi))
1012
1013                        break
1014
1015            if not figiJSON:
1016                for item in self.iList["Bonds"].keys():
1017                    if self._figi == self.iList["Bonds"][item]["figi"]:
1018                        figiJSON = self.iList["Bonds"][item]
1019
1020                        if self.moreDebug:
1021                            uLogger.debug("FIGI [{}] found in bonds list".format(self._figi))
1022
1023                        break
1024
1025            if not figiJSON:
1026                for item in self.iList["Etfs"].keys():
1027                    if self._figi == self.iList["Etfs"][item]["figi"]:
1028                        figiJSON = self.iList["Etfs"][item]
1029
1030                        if self.moreDebug:
1031                            uLogger.debug("FIGI [{}] found in etfs list".format(self._figi))
1032
1033                        break
1034
1035            if not figiJSON:
1036                for item in self.iList["Futures"].keys():
1037                    if self._figi == self.iList["Futures"][item]["figi"]:
1038                        figiJSON = self.iList["Futures"][item]
1039
1040                        if self.moreDebug:
1041                            uLogger.debug("FIGI [{}] found in futures list".format(self._figi))
1042
1043                        break
1044
1045        if figiJSON:
1046            self._figi = figiJSON["figi"]
1047            self._ticker = figiJSON["ticker"]
1048
1049            if requestPrice:
1050                figiJSON["currentPrice"] = self.GetCurrentPrices(show=False)
1051
1052                if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None:
1053                    figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"]
1054
1055                else:
1056                    figiJSON["currentPrice"]["changes"] = 0
1057
1058            if show:
1059                self.ShowInstrumentInfo(iJSON=figiJSON, show=True)  # print info as Markdown text
1060
1061        else:
1062            if show:
1063                uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self._figi))
1064
1065        return figiJSON

Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!

Parameters
  • requestPrice: if False then do not request current price of instrument (it's long operation).
  • show: if False then do not run ShowInstrumentInfo() method and do not print info to the console.
Returns

JSON formatted data with information about instrument.

def GetCurrentPrices(self, show: bool = True) -> dict:
1067    def GetCurrentPrices(self, show: bool = True) -> dict:
1068        """
1069        Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5:
1070        `{"buy": [{"price": 1243.8, "quantity": 193},
1071                  {"price": 1244.0, "quantity": 168},
1072                  {"price": 1244.8, "quantity": 5},
1073                  {"price": 1245.0, "quantity": 61},
1074                  {"price": 1245.4, "quantity": 60}],
1075          "sell": [{"price": 1243.6, "quantity": 8},
1076                   {"price": 1242.6, "quantity": 10},
1077                   {"price": 1242.4, "quantity": 18},
1078                   {"price": 1242.2, "quantity": 50},
1079                   {"price": 1242.0, "quantity": 113}],
1080          "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean:
1081        - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
1082        - sell: list of dicts with Buyers prices,
1083            - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
1084            - quantity: volume value by current price in lots,
1085        - limitUp: current trade session limit price, maximum,
1086        - limitDown: current trade session limit price, minimum,
1087        - lastPrice: last deal price of the instrument,
1088        - closePrice: previous trade session close price of the instrument.
1089
1090        See also: `SearchByTicker()` and `SearchByFIGI()`.
1091        REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1092        Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1093
1094        :param show: if `True` then print DOM to log and console.
1095        :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`.
1096                 If an error occurred then returns an empty record:
1097                 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`.
1098        """
1099        prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0}
1100
1101        if self.depth < 1:
1102            uLogger.error("Depth of Market (DOM) must be >=1!")
1103            raise Exception("Incorrect value")
1104
1105        if not (self._ticker or self._figi):
1106            uLogger.error("self._ticker or self._figi variables must be defined!")
1107            raise Exception("Ticker or FIGI required")
1108
1109        if self._ticker and not self._figi:
1110            instrumentByTicker = self.SearchByTicker(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1111            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
1112
1113        if not self._ticker and self._figi:
1114            instrumentByFigi = self.SearchByFIGI(requestPrice=False)  # WARNING! requestPrice=False to avoid recursion!
1115            self._ticker = instrumentByFigi["ticker"] if instrumentByFigi else ""
1116
1117        if not self._figi:
1118            uLogger.error("FIGI is not defined!")
1119            raise Exception("Ticker or FIGI required")
1120
1121        else:
1122            uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self._ticker, self._figi))
1123
1124            # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
1125            priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook"
1126            self.body = str({"figi": self._figi, "depth": self.depth})
1127            pricesResponse = self.SendAPIRequest(priceURL, reqType="POST")  # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
1128
1129            if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()):
1130                # list of dicts with sellers orders:
1131                prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]]
1132
1133                # list of dicts with buyers orders:
1134                prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]]
1135
1136                # max price of instrument at this time:
1137                prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None
1138
1139                # min price of instrument at this time:
1140                prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None
1141
1142                # last price of deal with instrument:
1143                prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0
1144
1145                # last close price of instrument:
1146                prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0
1147
1148            else:
1149                uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1150                uLogger.debug("Server response: {}".format(pricesResponse))
1151
1152            if show:
1153                if prices["buy"] or prices["sell"]:
1154                    info = [
1155                        "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format(
1156                            datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
1157                            self._ticker,
1158                            self._figi,
1159                            self.depth,
1160                        ),
1161                        "-" * 60, "\n",
1162                        "             Orders of Buyers | Orders of Sellers\n",
1163                        "-" * 60, "\n",
1164                        "        Sell prices (volumes) | Buy prices (volumes)\n",
1165                        "-" * 60, "\n",
1166                    ]
1167
1168                    if not prices["buy"]:
1169                        info.append("                              | No orders!\n")
1170                        sumBuy = 0
1171
1172                    else:
1173                        sumBuy = sum([x["quantity"] for x in prices["buy"]])
1174                        maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True)
1175                        for item in maxMinSorted:
1176                            info.append("                              | {} ({})\n".format(item["price"], item["quantity"]))
1177
1178                    if not prices["sell"]:
1179                        info.append("No orders!                    |\n")
1180                        sumSell = 0
1181
1182                    else:
1183                        sumSell = sum([x["quantity"] for x in prices["sell"]])
1184                        for item in prices["sell"]:
1185                            info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"])))
1186
1187                    info.extend([
1188                        "-" * 60, "\n",
1189                        "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)),
1190                        "-" * 60, "\n",
1191                    ])
1192
1193                    infoText = "".join(info)
1194
1195                    uLogger.info("Current prices in order book:\n\n{}".format(infoText))
1196
1197                else:
1198                    uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self._ticker, self._figi))
1199
1200        return prices

Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5: {"buy": [{"price": 1243.8, "quantity": 193}, {"price": 1244.0, "quantity": 168}, {"price": 1244.8, "quantity": 5}, {"price": 1245.0, "quantity": 61}, {"price": 1245.4, "quantity": 60}], "sell": [{"price": 1243.6, "quantity": 8}, {"price": 1242.6, "quantity": 10}, {"price": 1242.4, "quantity": 18}, {"price": 1242.2, "quantity": 50}, {"price": 1242.0, "quantity": 113}], "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:

  • buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
  • sell: list of dicts with Buyers prices,
    • price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
    • quantity: volume value by current price in lots,
  • limitUp: current trade session limit price, maximum,
  • limitDown: current trade session limit price, minimum,
  • lastPrice: last deal price of the instrument,
  • closePrice: previous trade session close price of the instrument.

See also: SearchByTicker() and SearchByFIGI(). REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse

Parameters
  • show: if True then print DOM to log and console.
Returns

orders book dict with lists of current buy and sell prices: {"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record: {"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.

def ShowInstrumentsInfo(self, show: bool = True) -> str:
1202    def ShowInstrumentsInfo(self, show: bool = True) -> str:
1203        """
1204        This method get and show information about all available broker instruments for current user account.
1205        If `instrumentsFile` string is not empty then also save information to this file.
1206
1207        :param show: if `True` then print results to console, if `False` — print only to file.
1208        :return: multi-lines string with all available broker instruments
1209        """
1210        if not self.iList:
1211            self.iList = self.Listing()
1212
1213        info = [
1214            "# All available instruments from Tinkoff Broker server for current user token\n\n",
1215            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1216        ]
1217
1218        # add instruments count by type:
1219        for iType in self.iList.keys():
1220            info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType])))
1221
1222        headerLine = "| Ticker       | Full name                                                 | FIGI         | Cur | Lot     | Step       |\n"
1223        splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n"
1224
1225        # generating info tables with all instruments by type:
1226        for iType in self.iList.keys():
1227            info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine])
1228
1229            for instrument in self.iList[iType].keys():
1230                iName = self.iList[iType][instrument]["name"]  # instrument's name
1231                if len(iName) > 57:
1232                    iName = "{}...".format(iName[:54])  # right trim for a long string
1233
1234                info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format(
1235                    self.iList[iType][instrument]["ticker"],
1236                    iName,
1237                    self.iList[iType][instrument]["figi"],
1238                    self.iList[iType][instrument]["currency"],
1239                    self.iList[iType][instrument]["lot"],
1240                    "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0,
1241                ))
1242
1243        infoText = "".join(info)
1244
1245        if show:
1246            uLogger.info(infoText)
1247
1248        if self.instrumentsFile:
1249            with open(self.instrumentsFile, "w", encoding="UTF-8") as fH:
1250                fH.write(infoText)
1251
1252            uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile)))
1253
1254            if self.useHTMLReports:
1255                htmlFilePath = self.instrumentsFile.replace(".md", ".html") if self.instrumentsFile.endswith(".md") else self.instrumentsFile + ".html"
1256                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1257                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="List of instruments", commonCSS=COMMON_CSS, markdown=infoText))
1258
1259                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1260
1261        return infoText

This method get and show information about all available broker instruments for current user account. If instrumentsFile string is not empty then also save information to this file.

Parameters
  • show: if True then print results to console, if False — print only to file.
Returns

multi-lines string with all available broker instruments

def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1263    def SearchInstruments(self, pattern: str, show: bool = True) -> dict:
1264        """
1265        This method search and show information about instruments by part of its ticker, FIGI or name.
1266        If `searchResultsFile` string is not empty then also save information to this file.
1267
1268        :param pattern: string with part of ticker, FIGI or instrument's name.
1269        :param show: if `True` then print results to console, if `False` — return list of result only.
1270        :return: list of dictionaries with all found instruments.
1271        """
1272        if not self.iList:
1273            self.iList = self.Listing()
1274
1275        searchResults = {iType: {} for iType in self.iList}  # same as iList but will contain only filtered instruments
1276        compiledPattern = re.compile(pattern, re.IGNORECASE)
1277
1278        for iType in self.iList:
1279            for instrument in self.iList[iType].values():
1280                searchResult = compiledPattern.search(" ".join(
1281                    [instrument["ticker"], instrument["figi"], instrument["name"]]
1282                ))
1283
1284                if searchResult:
1285                    searchResults[iType][instrument["ticker"]] = instrument
1286
1287        resultsLen = sum([len(searchResults[iType]) for iType in searchResults])
1288        info = [
1289            "# Search results\n\n",
1290            "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
1291            "* **Search pattern:** [{}]\n".format(pattern),
1292            "* **Found instruments:** [{}]\n\n".format(resultsLen),
1293            '**Note:** you can view info about found instruments with key "--info", e.g.: "tksbrokerapi -t TICKER --info" or "tksbrokerapi -f FIGI --info".\n'
1294        ]
1295        infoShort = info[:]
1296
1297        headerLine = "| Type       | Ticker       | Full name                                                      | FIGI         |\n"
1298        splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n"
1299        skippedLine = "| ...        | ...          | ...                                                            | ...          |\n"
1300
1301        if resultsLen == 0:
1302            info.append("\nNo results\n")
1303            infoShort.append("\nNo results\n")
1304            uLogger.warning("No results. Try changing your search pattern.")
1305
1306        else:
1307            for iType in searchResults:
1308                iTypeValuesCount = len(searchResults[iType].values())
1309                if iTypeValuesCount > 0:
1310                    info.extend(["\n## {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1311                    infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine])
1312
1313                    for instrument in searchResults[iType].values():
1314                        info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format(
1315                            instrument["type"],
1316                            instrument["ticker"],
1317                            "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"],  # right trim for a long string
1318                            instrument["figi"],
1319                        ))
1320
1321                    if iTypeValuesCount <= 5:
1322                        infoShort.extend(info[-iTypeValuesCount:])
1323
1324                    else:
1325                        infoShort.extend(info[-5:])
1326                        infoShort.append(skippedLine)
1327
1328        infoText = "".join(info)
1329        infoTextShort = "".join(infoShort)
1330
1331        if show:
1332            uLogger.info(infoTextShort)
1333            uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`")
1334
1335        if self.searchResultsFile:
1336            with open(self.searchResultsFile, "w", encoding="UTF-8") as fH:
1337                fH.write(infoText)
1338
1339            uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile)))
1340
1341            if self.useHTMLReports:
1342                htmlFilePath = self.searchResultsFile.replace(".md", ".html") if self.searchResultsFile.endswith(".md") else self.searchResultsFile + ".html"
1343                with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1344                    fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Search results", commonCSS=COMMON_CSS, markdown=infoText))
1345
1346                uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1347
1348        return searchResults

This method search and show information about instruments by part of its ticker, FIGI or name. If searchResultsFile string is not empty then also save information to this file.

Parameters
  • pattern: string with part of ticker, FIGI or instrument's name.
  • show: if True then print results to console, if False — return list of result only.
Returns

list of dictionaries with all found instruments.

def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1350    def GetUniqueFIGIs(self, instruments: list[str]) -> list:
1351        """
1352        Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
1353
1354        :param instruments: list of strings with tickers or FIGIs.
1355        :return: list with unique instrument FIGIs only.
1356        """
1357        requestedInstruments = []
1358        for iName in instruments:
1359            if iName not in self.aliases.keys():
1360                if iName not in requestedInstruments:
1361                    requestedInstruments.append(iName)
1362
1363            else:
1364                if iName not in requestedInstruments:
1365                    if self.aliases[iName] not in requestedInstruments:
1366                        requestedInstruments.append(self.aliases[iName])
1367
1368        uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments))
1369
1370        onlyUniqueFIGIs = []
1371        for iName in requestedInstruments:
1372            if iName in TKS_TICKERS_OR_FIGI_EXCLUDED:
1373                continue
1374
1375            self._ticker = iName
1376            iData = self.SearchByTicker(requestPrice=False)  # trying to find instrument by ticker
1377
1378            if not iData:
1379                self._ticker = ""
1380                self._figi = iName
1381
1382                iData = self.SearchByFIGI(requestPrice=False)  # trying to find instrument by FIGI
1383
1384                if not iData:
1385                    self._figi = ""
1386                    uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName))
1387
1388            if iData and iData["figi"] not in onlyUniqueFIGIs:
1389                onlyUniqueFIGIs.append(iData["figi"])
1390
1391        uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs))
1392
1393        return onlyUniqueFIGIs

Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.

Parameters
  • instruments: list of strings with tickers or FIGIs.
Returns

list with unique instrument FIGIs only.

def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1395    def GetListOfPrices(self, instruments: list[str], show: bool = False) -> list[dict]:
1396        """
1397        This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
1398
1399        See limits: https://tinkoff.github.io/investAPI/limits/
1400
1401        If `pricesFile` string is not empty then also save information to this file.
1402
1403        :param instruments: list of strings with tickers or FIGIs.
1404        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1405        :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1406                 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods.
1407        """
1408        if instruments is None or not instruments:
1409            uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!")
1410            raise Exception("Ticker or FIGI required")
1411
1412        onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments)
1413
1414        uLogger.debug("Requesting current prices from Tinkoff Broker server...")
1415
1416        iList = []  # trying to get info and current prices about all unique instruments:
1417        for self._figi in onlyUniqueFIGIs:
1418            iData = self.SearchByFIGI(requestPrice=True)
1419            iList.append(iData)
1420
1421        self.ShowListOfPrices(iList, show)
1422
1423        return iList

This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!

See limits: https://tinkoff.github.io/investAPI/limits/

If pricesFile string is not empty then also save information to this file.

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

list of instruments looks like [{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker() or SearchByFIGI() methods.

def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1425    def ShowListOfPrices(self, iList: list, show: bool = True) -> str:
1426        """
1427        Show table contains current prices of given instruments.
1428
1429        :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`.
1430                      One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods.
1431        :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`.
1432        :return: multilines text in Markdown format as a table contains current prices.
1433        """
1434        infoText = ""
1435
1436        if show or self.pricesFile:
1437            info = [
1438                "# Current prices\n\n* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")),
1439                "| Ticker       | FIGI         | Type       | Prev. close | Last price  | Chg. %   | Day limits min/max  | Actual sell / buy   | Curr. |\n",
1440                "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n",
1441            ]
1442
1443            for item in iList:
1444                info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format(
1445                    item["ticker"],
1446                    item["figi"],
1447                    item["type"],
1448                    "{:.2f}".format(float(item["currentPrice"]["closePrice"])),
1449                    "{:.2f}".format(float(item["currentPrice"]["lastPrice"])),
1450                    "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])),
1451                    "{} / {}".format(
1452                        item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A",
1453                        item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A",
1454                    ),
1455                    "{} / {}".format(
1456                        item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A",
1457                        item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A",
1458                    ),
1459                    item["currency"],
1460                ))
1461
1462            infoText = "".join(info)
1463
1464            if show:
1465                uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText))
1466
1467            if self.pricesFile:
1468                with open(self.pricesFile, "w", encoding="UTF-8") as fH:
1469                    fH.write(infoText)
1470
1471                uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile)))
1472
1473                if self.useHTMLReports:
1474                    htmlFilePath = self.pricesFile.replace(".md", ".html") if self.pricesFile.endswith(".md") else self.pricesFile + ".html"
1475                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
1476                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Current prices", commonCSS=COMMON_CSS, markdown=infoText))
1477
1478                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
1479
1480        return infoText

Show table contains current prices of given instruments.

Parameters
  • **iList: list of instruments looks like [{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned by SearchByTicker(requestPrice=True) or by SearchByFIGI(requestPrice=True) methods.
  • show: if True then prints prices to console, if False — prints only to file pricesFile.
Returns

multilines text in Markdown format as a table contains current prices.

def RequestTradingStatus(self) -> dict:
1482    def RequestTradingStatus(self) -> dict:
1483        """
1484        Requesting trading status for the instrument defined by `figi` variable.
1485
1486        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
1487
1488        Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
1489
1490        :return: dictionary with trading status attributes. Response example:
1491                 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING",
1492                  "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}`
1493        """
1494        if self._figi is None or not self._figi:
1495            uLogger.error("Variable `figi` must be defined for using this method!")
1496            raise Exception("FIGI required")
1497
1498        uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self._figi))
1499
1500        self.body = str({"figi": self._figi, "instrumentId": self._figi})
1501        tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus"
1502        tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST")
1503
1504        if self.moreDebug:
1505            uLogger.debug("Records about current trading status successfully received")
1506
1507        return tradingStatus

Requesting trading status for the instrument defined by figi variable.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus

Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest

Returns

dictionary with trading status attributes. Response example: {"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}

def RequestPortfolio(self) -> dict:
1509    def RequestPortfolio(self) -> dict:
1510        """
1511        Requesting actual user's portfolio for current `accountId`.
1512
1513        REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
1514
1515        Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
1516
1517        :return: dictionary with user's portfolio.
1518        """
1519        if self.accountId is None or not self.accountId:
1520            uLogger.error("Variable `accountId` must be defined for using this method!")
1521            raise Exception("Account ID required")
1522
1523        uLogger.debug("Requesting current actual user's portfolio. Wait, please...")
1524
1525        self.body = str({"accountId": self.accountId})
1526        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio"
1527        rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST")
1528
1529        if self.moreDebug:
1530            uLogger.debug("Records about user's portfolio successfully received")
1531
1532        return rawPortfolio

Requesting actual user's portfolio for current accountId.

REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio

Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest

Returns

dictionary with user's portfolio.

def RequestPositions(self) -> dict:
1534    def RequestPositions(self) -> dict:
1535        """
1536        Requesting open positions by currencies and instruments for current `accountId`.
1537
1538        REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
1539
1540        Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
1541
1542        :return: dictionary with open positions by instruments.
1543        """
1544        if self.accountId is None or not self.accountId:
1545            uLogger.error("Variable `accountId` must be defined for using this method!")
1546            raise Exception("Account ID required")
1547
1548        uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...")
1549
1550        self.body = str({"accountId": self.accountId})
1551        positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions"
1552        rawPositions = self.SendAPIRequest(positionsURL, reqType="POST")
1553
1554        if self.moreDebug:
1555            uLogger.debug("Records about current open positions successfully received")
1556
1557        return rawPositions

Requesting open positions by currencies and instruments for current accountId.

REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions

Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest

Returns

dictionary with open positions by instruments.

def RequestPendingOrders(self) -> list:
1559    def RequestPendingOrders(self) -> list:
1560        """
1561        Requesting current actual pending limit orders for current `accountId`.
1562
1563        REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
1564
1565        Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
1566
1567        :return: list of dictionaries with pending limit orders.
1568        """
1569        if self.accountId is None or not self.accountId:
1570            uLogger.error("Variable `accountId` must be defined for using this method!")
1571            raise Exception("Account ID required")
1572
1573        uLogger.debug("Requesting current actual pending limit orders. Wait, please...")
1574
1575        self.body = str({"accountId": self.accountId})
1576        ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders"
1577        rawResponse = self.SendAPIRequest(ordersURL, reqType="POST")
1578
1579        if "orders" in rawResponse.keys():
1580            rawOrders = rawResponse["orders"]
1581            uLogger.debug("[{}] records about pending limit orders received".format(len(rawOrders)))
1582
1583        else:
1584            rawOrders = []
1585            uLogger.debug("No pending limit orders returned! rawResponse = {}".format(rawResponse))
1586
1587        return rawOrders

Requesting current actual pending limit orders for current accountId.

REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders

Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest

Returns

list of dictionaries with pending limit orders.

def RequestStopOrders(self) -> list:
1589    def RequestStopOrders(self) -> list:
1590        """
1591        Requesting current actual stop orders for current `accountId`.
1592
1593        REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
1594
1595        Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
1596
1597        :return: list of dictionaries with stop orders.
1598        """
1599        if self.accountId is None or not self.accountId:
1600            uLogger.error("Variable `accountId` must be defined for using this method!")
1601            raise Exception("Account ID required")
1602
1603        uLogger.debug("Requesting current actual stop orders. Wait, please...")
1604
1605        self.body = str({"accountId": self.accountId})
1606        stopOrdersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders"
1607        rawResponse = self.SendAPIRequest(stopOrdersURL, reqType="POST")
1608
1609        if "stopOrders" in rawResponse.keys():
1610            rawStopOrders = rawResponse["stopOrders"]
1611            uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders)))
1612
1613        else:
1614            rawStopOrders = []
1615            uLogger.debug("No stop orders returned! rawResponse = {}".format(rawResponse))
1616
1617        return rawStopOrders

Requesting current actual stop orders for current accountId.

REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders

Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest

Returns

list of dictionaries with stop orders.

def Overview(self, show: bool = False, details: str = 'full') -> dict:
1619    def Overview(self, show: bool = False, details: str = "full") -> dict:
1620        """
1621        Get portfolio: all open positions, orders and some statistics for current `accountId`.
1622        If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile`
1623        and `overviewBondsCalendarFile` are defined then also save information to file.
1624
1625        WARNING! It is not recommended to run this method too many times in a loop! The server receives
1626        many requests about the state of the portfolio, and then, based on the received data, a large number
1627        of calculation and statistics are collected.
1628
1629        :param show: if `False` then only dictionary returns, if `True` then show more debug information.
1630        :param details: how detailed should the information be?
1631        - `full` — shows full available information about portfolio status (by default),
1632        - `positions` — shows only open positions,
1633        - `orders` — shows only sections of open limits and stop orders.
1634        - `digest` — show a short digest of the portfolio status,
1635        - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories,
1636        - `calendar` — shows only the bonds calendar section (if these present in portfolio),
1637        :return: dictionary with client's raw portfolio and some statistics.
1638        """
1639        if self.accountId is None or not self.accountId:
1640            uLogger.error("Variable `accountId` must be defined for using this method!")
1641            raise Exception("Account ID required")
1642
1643        view = {
1644            "raw": {  # --- raw portfolio responses from broker with user portfolio data:
1645                "headers": {},  # list of dictionaries, response headers without "positions" section
1646                "Currencies": [],  # list of dictionaries, open trades with currencies from "positions" section
1647                "Shares": [],  # list of dictionaries, open trades with shares from "positions" section
1648                "Bonds": [],  # list of dictionaries, open trades with bonds from "positions" section
1649                "Etfs": [],  # list of dictionaries, open trades with etfs from "positions" section
1650                "Futures": [],  # list of dictionaries, open trades with futures from "positions" section
1651                "positions": {},  # raw response from broker: dictionary with current available or blocked currencies and instruments for client
1652                "orders": [],  # raw response from broker: list of dictionaries with all pending (market) orders
1653                "stopOrders": [],  # raw response from broker: list of dictionaries with all stop orders
1654                "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}},  # dict with prices of all currencies in RUB
1655            },
1656            "stat": {  # --- some statistics calculated using "raw" sections:
1657                "portfolioCostRUB": 0.,  # portfolio cost in RUB (Russian Rouble)
1658                "availableRUB": 0.,  # available rubles (without other currencies)
1659                "blockedRUB": 0.,  # blocked sum in Russian Rouble
1660                "totalChangesRUB": 0.,  # changes for all open trades in RUB
1661                "totalChangesPercentRUB": 0.,  # changes for all open trades in percents
1662                "allCurrenciesCostRUB": 0.,  # costs of all currencies (include rubles) in RUB
1663                "sharesCostRUB": 0.,  # costs of all shares in RUB
1664                "bondsCostRUB": 0.,  # costs of all bonds in RUB
1665                "etfsCostRUB": 0.,  # costs of all etfs in RUB
1666                "futuresCostRUB": 0.,  # costs of all futures in RUB
1667                "Currencies": [],  # list of dictionaries of all currencies statistics
1668                "Shares": [],  # list of dictionaries of all shares statistics
1669                "Bonds": [],  # list of dictionaries of all bonds statistics
1670                "Etfs": [],  # list of dictionaries of all etfs statistics
1671                "Futures": [],  # list of dictionaries of all futures statistics
1672                "orders": [],  # list of dictionaries of all pending (market) orders and it's parameters
1673                "stopOrders": [],  # list of dictionaries of all stop orders and it's parameters
1674                "blockedCurrencies": {},  # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21}
1675                "blockedInstruments": {},  # dict with blocked  by FIGI, e.g. {}
1676                "funds": {},  # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1677            },
1678            "analytics": {  # --- some analytics of portfolio:
1679                "distrByAssets": {},  # portfolio distribution by assets
1680                "distrByCompanies": {},  # portfolio distribution by companies
1681                "distrBySectors": {},  # portfolio distribution by sectors
1682                "distrByCurrencies": {},  # portfolio distribution by currencies
1683                "distrByCountries": {},  # portfolio distribution by countries
1684                "bondsCalendar": None,  # bonds payment calendar as Pandas DataFrame (if these present in portfolio)
1685            }
1686        }
1687
1688        details = details.lower()
1689        availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"]
1690        if details not in availableDetails:
1691            details = "full"
1692            uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails))
1693
1694        uLogger.debug("Requesting portfolio of a client. Wait, please...")
1695
1696        portfolioResponse = self.RequestPortfolio()  # current user's portfolio (dict)
1697        view["raw"]["positions"] = self.RequestPositions()  # current open positions by instruments (dict)
1698        view["raw"]["orders"] = self.RequestPendingOrders()  # current actual pending limit orders (list)
1699        view["raw"]["stopOrders"] = self.RequestStopOrders()  # current actual stop orders (list)
1700
1701        # save response headers without "positions" section:
1702        for key in portfolioResponse.keys():
1703            if key != "positions":
1704                view["raw"]["headers"][key] = portfolioResponse[key]
1705
1706            else:
1707                continue
1708
1709        # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation
1710        # Type of instrument must be only one of supported types in TKS_INSTRUMENTS
1711        for item in portfolioResponse["positions"]:
1712            if item["instrumentType"] == "currency":
1713                self._figi = item["figi"]
1714                if not self._figi and item["ticker"]:
1715                    self._ticker = item["ticker"]
1716                    self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1717
1718                curr = self.SearchByFIGI(requestPrice=False)
1719
1720                # current price of currency in RUB:
1721                view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = {
1722                    "name": curr["name"],
1723                    "currentPrice": NanoToFloat(
1724                        item["currentPrice"]["units"],
1725                        item["currentPrice"]["nano"]
1726                    ),
1727                }
1728
1729                view["raw"]["Currencies"].append(item)
1730
1731            elif item["instrumentType"] == "share":
1732                view["raw"]["Shares"].append(item)
1733
1734            elif item["instrumentType"] == "bond":
1735                view["raw"]["Bonds"].append(item)
1736
1737            elif item["instrumentType"] == "etf":
1738                view["raw"]["Etfs"].append(item)
1739
1740            elif item["instrumentType"] == "futures":
1741                view["raw"]["Futures"].append(item)
1742
1743            else:
1744                continue
1745
1746        # how many volume of currencies (by ISO currency name) are blocked:
1747        for item in view["raw"]["positions"]["blocked"]:
1748            blocked = NanoToFloat(item["units"], item["nano"])
1749            if blocked > 0:
1750                view["stat"]["blockedCurrencies"][item["currency"]] = blocked
1751
1752        # how many volume of instruments (by FIGI) are blocked:
1753        for item in view["raw"]["positions"]["securities"]:
1754            blocked = int(item["blocked"])
1755            if blocked > 0:
1756                view["stat"]["blockedInstruments"][item["figi"]] = blocked
1757
1758        allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]}
1759
1760        if "rub" in allBlocked.keys():
1761            view["stat"]["blockedRUB"] = allBlocked["rub"]  # blocked rubles
1762
1763        # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies:
1764        view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"])
1765        view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"])
1766        view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"])
1767        view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"])
1768        view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"])
1769        view["stat"]["portfolioCostRUB"] = sum([
1770            view["stat"]["allCurrenciesCostRUB"],
1771            view["stat"]["sharesCostRUB"],
1772            view["stat"]["bondsCostRUB"],
1773            view["stat"]["etfsCostRUB"],
1774            view["stat"]["futuresCostRUB"],
1775        ])
1776
1777        # --- calculating some portfolio statistics:
1778        byComp = {}  # distribution by companies
1779        bySect = {}  # distribution by sectors
1780        byCurr = {}  # distribution by currencies (include RUB)
1781        unknownCountryName = "All other countries"  # default name for instruments without "countryOfRisk" and "countryOfRiskName"
1782        byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}}  # distribution by countries (currencies are included in their countries)
1783
1784        for item in portfolioResponse["positions"]:
1785            self._figi = item["figi"]
1786            if not self._figi and item["ticker"]:
1787                self._ticker = item["ticker"]
1788                self._figi = self.SearchByTicker()["figi"]  # Get FIGI to avoid warnings
1789
1790            instrument = self.SearchByFIGI(requestPrice=False)  # full raw info about instrument by FIGI
1791
1792            if instrument:
1793                if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys():
1794                    blocked = allBlocked[instrument["nominal"]["currency"]]  # blocked volume of currency
1795
1796                elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys():
1797                    blocked = allBlocked[item["figi"]]  # blocked volume of other instruments
1798
1799                else:
1800                    blocked = 0
1801
1802                volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"])  # available volume of instrument
1803                lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"])  # available volume in lots of instrument
1804                direction = "Long" if lots >= 0 else "Short"  # direction of an instrument's position: short or long
1805                curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"])  # current instrument's price
1806                average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"])  # current average position price
1807                profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"])  # expected profit at current moment
1808                currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"]  # currency name rub, usd, eur etc.
1809                cost = curPrice if "currentNkd" not in item.keys() else (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume  # current cost of all volume of instrument in basic asset
1810                baseCurrencyName = item["currentPrice"]["currency"]  # name of base currency (rub)
1811                countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName
1812                costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"]  # cost in rubles
1813                percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.  # instrument's part in percent of full portfolio cost
1814
1815                statData = {
1816                    "figi": item["figi"],  # FIGI from REST API "GetPortfolio" method
1817                    "ticker": instrument["ticker"],  # ticker by FIGI
1818                    "currency": currency,  # currency name rub, usd, eur etc. for instrument price
1819                    "volume": volume,  # available volume of instrument
1820                    "lots": lots,  # volume in lots of instrument
1821                    "direction": direction,  # direction of an instrument's position: short or long
1822                    "blocked": blocked,  # blocked volume of currency or instrument
1823                    "currentPrice": curPrice,  # current instrument's price in basic asset
1824                    "average": average,  # current average position price
1825                    "cost": cost,  # current cost of all volume of instrument in basic asset
1826                    "baseCurrencyName": baseCurrencyName,  # name of base currency (rub)
1827                    "costRUB": costRUB,  # cost of instrument in ruble
1828                    "percentCostRUB": percentCostRUB,  # instrument's part in percent of full portfolio cost in RUB
1829                    "profit": profit,  # expected profit at current moment
1830                    "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0,  # expected percents of profit at current moment for this instrument
1831                    "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other",
1832                    "name": instrument["name"] if "name" in instrument.keys() else "",  # human-readable names of instruments
1833                    "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "",  # ISO name for currencies only
1834                    "country": countryName,  # e.g. "[RU] Российская Федерация" or unknownCountryName
1835                    "step": instrument["step"],  # minimum price increment
1836                }
1837
1838                # adding distribution by unique countries:
1839                if statData["country"] not in byCountry.keys():
1840                    byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB}
1841
1842                else:
1843                    byCountry[statData["country"]]["cost"] += costRUB
1844                    byCountry[statData["country"]]["percent"] += percentCostRUB
1845
1846                if item["instrumentType"] != "currency":
1847                    # adding distribution by unique companies:
1848                    if statData["name"]:
1849                        if statData["name"] not in byComp.keys():
1850                            byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB}
1851
1852                        else:
1853                            byComp[statData["name"]]["cost"] += costRUB
1854                            byComp[statData["name"]]["percent"] += percentCostRUB
1855
1856                    # adding distribution by unique sectors:
1857                    if statData["sector"] not in bySect.keys():
1858                        bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB}
1859
1860                    else:
1861                        bySect[statData["sector"]]["cost"] += costRUB
1862                        bySect[statData["sector"]]["percent"] += percentCostRUB
1863
1864                # adding distribution by unique currencies:
1865                if currency not in byCurr.keys():
1866                    byCurr[currency] = {
1867                        "name": view["raw"]["currenciesCurrentPrices"][currency]["name"],
1868                        "cost": costRUB,
1869                        "percent": percentCostRUB
1870                    }
1871
1872                else:
1873                    byCurr[currency]["cost"] += costRUB
1874                    byCurr[currency]["percent"] += percentCostRUB
1875
1876                # saving statistics for every instrument:
1877                if item["instrumentType"] == "currency":
1878                    view["stat"]["Currencies"].append(statData)
1879
1880                    # update dict with free funds for trading (total - blocked) by currencies
1881                    # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}}
1882                    view["stat"]["funds"][currency] = {
1883                        "total": volume,
1884                        "totalCostRUB": costRUB,  # total volume cost in rubles
1885                        "free": volume - blocked,
1886                        "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0,  # free volume cost in rubles
1887                    }
1888
1889                elif item["instrumentType"] == "share":
1890                    view["stat"]["Shares"].append(statData)
1891
1892                elif item["instrumentType"] == "bond":
1893                    view["stat"]["Bonds"].append(statData)
1894
1895                elif item["instrumentType"] == "etf":
1896                    view["stat"]["Etfs"].append(statData)
1897
1898                elif item["instrumentType"] == "Futures":
1899                    view["stat"]["Futures"].append(statData)
1900
1901                else:
1902                    continue
1903
1904        # total changes in Russian Ruble:
1905        view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]])  # available RUB without other currencies
1906        view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0.
1907        startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100)
1908        view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost
1909        view["stat"]["funds"]["rub"] = {
1910            "total": view["stat"]["availableRUB"],
1911            "totalCostRUB": view["stat"]["availableRUB"],
1912            "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1913            "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"],
1914        }
1915
1916        # --- pending limit orders sector data:
1917        uniquePendingOrdersFIGIs = []  # unique FIGIs of pending limit orders to avoid many times price requests
1918        uniquePendingOrders = {}  # unique instruments with FIGIs as dictionary keys
1919
1920        for item in view["raw"]["orders"]:
1921            self._figi = item["figi"]
1922
1923            if item["figi"] not in uniquePendingOrdersFIGIs:
1924                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1925
1926                uniquePendingOrdersFIGIs.append(item["figi"])
1927                uniquePendingOrders[item["figi"]] = instrument
1928
1929            else:
1930                instrument = uniquePendingOrders[item["figi"]]
1931
1932            if instrument:
1933                action = TKS_ORDER_DIRECTIONS[item["direction"]]
1934                orderType = TKS_ORDER_TYPES[item["orderType"]]
1935                orderState = TKS_ORDER_STATES[item["executionReportStatus"]]
1936                orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1937
1938                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1939                if item["direction"] == "ORDER_DIRECTION_BUY":
1940                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
1941
1942                else:
1943                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
1944
1945                # requested price for order execution:
1946                target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"])
1947
1948                # necessary changes in percent to reach target from current price:
1949                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
1950
1951                view["stat"]["orders"].append({
1952                    "orderID": item["orderId"],  # orderId number parameter of current order
1953                    "figi": item["figi"],  # FIGI identification
1954                    "ticker": instrument["ticker"],  # ticker name by FIGI
1955                    "lotsRequested": item["lotsRequested"],  # requested lots value
1956                    "lotsExecuted": item["lotsExecuted"],  # how many lots are executed
1957                    "currentPrice": lastPrice,  # current instrument's price for defined action
1958                    "targetPrice": target,  # requested price for order execution in base currency
1959                    "baseCurrencyName": item["initialSecurityPrice"]["currency"],  # name of base currency
1960                    "percentChanges": changes,  # changes in percent to target from current price
1961                    "currency": item["currency"],  # instrument's currency name
1962                    "action": action,  # sell / buy / Unknown from TKS_ORDER_DIRECTIONS
1963                    "type": orderType,  # type of order from TKS_ORDER_TYPES
1964                    "status": orderState,  # order status from TKS_ORDER_STATES
1965                    "date": orderDate,  # string with order date and time from UTC format (without nano seconds part)
1966                })
1967
1968        # --- stop orders sector data:
1969        uniqueStopOrdersFIGIs = []  # unique FIGIs of stop orders to avoid many times price requests
1970        uniqueStopOrders = {}  # unique instruments with FIGIs as dictionary keys
1971
1972        for item in view["raw"]["stopOrders"]:
1973            self._figi = item["figi"]
1974
1975            if item["figi"] not in uniqueStopOrdersFIGIs:
1976                instrument = self.SearchByFIGI(requestPrice=True)  # full raw info about instrument by FIGI, price requests only one time
1977
1978                uniqueStopOrdersFIGIs.append(item["figi"])
1979                uniqueStopOrders[item["figi"]] = instrument
1980
1981            else:
1982                instrument = uniqueStopOrders[item["figi"]]
1983
1984            if instrument:
1985                action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]]
1986                orderType = TKS_STOP_ORDER_TYPES[item["orderType"]]
1987                createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0]  # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z"
1988
1989                # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order
1990                if "expirationTime" in item.keys():
1991                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"]
1992                    expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0]
1993
1994                else:
1995                    expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"]
1996                    expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"]
1997
1998                # current instrument's price (last sellers order if buy, and last buyers order if sell):
1999                if item["direction"] == "STOP_ORDER_DIRECTION_BUY":
2000                    lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A"
2001
2002                else:
2003                    lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A"
2004
2005                # requested price when stop-order executed:
2006                target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"])
2007
2008                # price for limit-order, set up when stop-order executed:
2009                limit = NanoToFloat(item["price"]["units"], item["price"]["nano"])
2010
2011                # necessary changes in percent to reach target from current price:
2012                changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0
2013
2014                view["stat"]["stopOrders"].append({
2015                    "orderID": item["stopOrderId"],  # stopOrderId number parameter of current stop-order
2016                    "figi": item["figi"],  # FIGI identification
2017                    "ticker": instrument["ticker"],  # ticker name by FIGI
2018                    "lotsRequested": item["lotsRequested"],  # requested lots value
2019                    "currentPrice": lastPrice,  # current instrument's price for defined action
2020                    "targetPrice": target,  # requested price for stop-order execution in base currency
2021                    "limitPrice": limit,  # price for limit-order, set up when stop-order executed, 0 if market order
2022                    "baseCurrencyName": item["stopPrice"]["currency"],  # name of base currency
2023                    "percentChanges": changes,  # changes in percent to target from current price
2024                    "currency": item["currency"],  # instrument's currency name
2025                    "action": action,  # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS
2026                    "type": orderType,  # type of order from TKS_STOP_ORDER_TYPES
2027                    "expType": expType,  # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES
2028                    "createDate": createDate,  # string with created order date and time from UTC format (without nano seconds part)
2029                    "expDate": expDate,  # string with expiration order date and time from UTC format (without nano seconds part)
2030                })
2031
2032        # --- calculating data for analytics section:
2033        # portfolio distribution by assets:
2034        view["analytics"]["distrByAssets"] = {
2035            "Ruble": {
2036                "uniques": 1,
2037                "cost": view["stat"]["availableRUB"],
2038                "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2039            },
2040            "Currencies": {
2041                "uniques": len(view["stat"]["Currencies"]),  # all foreign currencies without RUB
2042                "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"],
2043                "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2044            },
2045            "Shares": {
2046                "uniques": len(view["stat"]["Shares"]),
2047                "cost": view["stat"]["sharesCostRUB"],
2048                "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2049            },
2050            "Bonds": {
2051                "uniques": len(view["stat"]["Bonds"]),
2052                "cost": view["stat"]["bondsCostRUB"],
2053                "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2054            },
2055            "Etfs": {
2056                "uniques": len(view["stat"]["Etfs"]),
2057                "cost": view["stat"]["etfsCostRUB"],
2058                "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2059            },
2060            "Futures": {
2061                "uniques": len(view["stat"]["Futures"]),
2062                "cost": view["stat"]["futuresCostRUB"],
2063                "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2064            },
2065        }
2066
2067        # portfolio distribution by companies:
2068        view["analytics"]["distrByCompanies"]["All money cash"] = {
2069            "ticker": "",
2070            "cost": view["stat"]["allCurrenciesCostRUB"],
2071            "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0.,
2072        }
2073        view["analytics"]["distrByCompanies"].update(byComp)
2074
2075        # portfolio distribution by sectors:
2076        view["analytics"]["distrBySectors"]["All money cash"] = {
2077            "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"],
2078            "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"],
2079        }
2080        view["analytics"]["distrBySectors"].update(bySect)
2081
2082        # portfolio distribution by currencies:
2083        if "rub" not in view["analytics"]["distrByCurrencies"].keys():
2084            view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0}
2085
2086            if self.moreDebug:
2087                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!")
2088
2089        view["analytics"]["distrByCurrencies"].update(byCurr)
2090        view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2091        view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2092
2093        # portfolio distribution by countries:
2094        if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys():
2095            view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0}
2096
2097            if self.moreDebug:
2098                uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!")
2099
2100        view["analytics"]["distrByCountries"].update(byCountry)
2101        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"]
2102        view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"]
2103
2104        # --- Prepare text statistics overview in human-readable:
2105        if show:
2106            actualOnDate = datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)
2107
2108            # Whatever the value `details`, header not changes:
2109            info = [
2110                "# Client's portfolio\n\n",
2111                "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2112                "* **Account ID:** [{}]\n".format(self.accountId),
2113            ]
2114
2115            if details in ["full", "positions", "digest"]:
2116                info.extend([
2117                    "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2118                    "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format(
2119                        "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2120                        view["stat"]["totalChangesRUB"],
2121                        "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2122                        view["stat"]["totalChangesPercentRUB"],
2123                    ),
2124                ])
2125
2126            if details in ["full", "positions"]:
2127                info.extend([
2128                    "## Open positions\n\n",
2129                    "| Ticker [FIGI]               | Volume (blocked)                | Lots     | Curr. price  | Avg. price   | Current volume cost | Profit (%)                   |\n",
2130                    "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n",
2131                    "| **Ruble:**                  | {:>31} |          |              |              |                     |                              |\n".format(
2132                        "{:.2f} ({:.2f}) rub".format(
2133                            view["stat"]["availableRUB"],
2134                            view["stat"]["blockedRUB"],
2135                        )
2136                    )
2137                ])
2138
2139                def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list:
2140                    return [
2141                        "|                             |                                 |          |              |              |                     |                              |\n",
2142                        "| {:<27} |                                 |          |              |              | {:>19} |                              |\n".format(
2143                            noTradeStr if noTradeStr else typeStr,
2144                            "" if noTradeStr else "{:.2f} RUB".format(CostRUB),
2145                        ),
2146                    ]
2147
2148                def _InfoStr(data: dict, isCurr: bool = False) -> str:
2149                    return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format(
2150                        "{} [{}]".format(data["ticker"], data["figi"]),
2151                        "{:.2f} ({:.2f}) {}".format(
2152                            data["volume"],
2153                            data["blocked"],
2154                            data["currency"],
2155                        ) if isCurr else "{:.0f} ({:.0f})".format(
2156                            data["volume"],
2157                            data["blocked"],
2158                        ),
2159                        "—" if isCurr else "{:.4f}".format(data["lots"]).rstrip("0").rstrip("."),
2160                        "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a",
2161                        "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a",
2162                        "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]),
2163                        "{}{:.2f} {} ({}{:.2f}%)".format(
2164                            "+" if data["profit"] > 0 else "",
2165                            data["profit"], data["baseCurrencyName"],
2166                            "+" if data["percentProfit"] > 0 else "",
2167                            data["percentProfit"],
2168                        ),
2169                    )
2170
2171                # --- Show currencies section:
2172                if view["stat"]["Currencies"]:
2173                    info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**"))
2174                    for item in view["stat"]["Currencies"]:
2175                        info.append(_InfoStr(item, isCurr=True))
2176
2177                else:
2178                    info.extend(_SplitStr(noTradeStr="**Currencies:** no trades"))
2179
2180                # --- Show shares section:
2181                if view["stat"]["Shares"]:
2182                    info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**"))
2183
2184                    for item in view["stat"]["Shares"]:
2185                        info.append(_InfoStr(item))
2186
2187                else:
2188                    info.extend(_SplitStr(noTradeStr="**Shares:** no trades"))
2189
2190                # --- Show bonds section:
2191                if view["stat"]["Bonds"]:
2192                    info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**"))
2193
2194                    for item in view["stat"]["Bonds"]:
2195                        info.append(_InfoStr(item))
2196
2197                else:
2198                    info.extend(_SplitStr(noTradeStr="**Bonds:** no trades"))
2199
2200                # --- Show etfs section:
2201                if view["stat"]["Etfs"]:
2202                    info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**"))
2203
2204                    for item in view["stat"]["Etfs"]:
2205                        info.append(_InfoStr(item))
2206
2207                else:
2208                    info.extend(_SplitStr(noTradeStr="**Etfs:** no trades"))
2209
2210                # --- Show futures section:
2211                if view["stat"]["Futures"]:
2212                    info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**"))
2213
2214                    for item in view["stat"]["Futures"]:
2215                        info.append(_InfoStr(item))
2216
2217                else:
2218                    info.extend(_SplitStr(noTradeStr="**Futures:** no trades"))
2219
2220            if details in ["full", "orders"]:
2221                # --- Show pending limit orders section:
2222                if view["stat"]["orders"]:
2223                    info.extend([
2224                        "\n## Opened pending limit-orders: [{}]\n".format(len(view["stat"]["orders"])),
2225                        "\n| Ticker [FIGI]               | Order ID       | Lots (exec.) | Current price (% delta) | Target price  | Action    | Type      | Create date (UTC)       |\n",
2226                        "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n",
2227                    ])
2228
2229                    for item in view["stat"]["orders"]:
2230                        info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format(
2231                            "{} [{}]".format(item["ticker"], item["figi"]),
2232                            item["orderID"],
2233                            "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]),
2234                            "{} {} ({}{:.2f}%)".format(
2235                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2236                                item["baseCurrencyName"],
2237                                "+" if item["percentChanges"] > 0 else "",
2238                                float(item["percentChanges"]),
2239                            ),
2240                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2241                            item["action"],
2242                            item["type"],
2243                            item["date"],
2244                        ))
2245
2246                else:
2247                    info.append("\n## Total pending limit-orders: [0]\n")
2248
2249                # --- Show stop orders section:
2250                if view["stat"]["stopOrders"]:
2251                    info.extend([
2252                        "\n## Opened stop-orders: [{}]\n".format(len(view["stat"]["stopOrders"])),
2253                        "\n| Ticker [FIGI]               | Stop order ID                        | Lots   | Current price (% delta) | Target price  | Limit price   | Action    | Type        | Expire type  | Create date (UTC)   | Expiration (UTC)    |\n",
2254                        "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n",
2255                    ])
2256
2257                    for item in view["stat"]["stopOrders"]:
2258                        info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format(
2259                            "{} [{}]".format(item["ticker"], item["figi"]),
2260                            item["orderID"],
2261                            item["lotsRequested"],
2262                            "{} {} ({}{:.2f}%)".format(
2263                                "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])),
2264                                item["baseCurrencyName"],
2265                                "+" if item["percentChanges"] > 0 else "",
2266                                float(item["percentChanges"]),
2267                            ),
2268                            "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]),
2269                            "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"],
2270                            item["action"],
2271                            item["type"],
2272                            item["expType"],
2273                            item["createDate"],
2274                            item["expDate"],
2275                        ))
2276
2277                else:
2278                    info.append("\n## Total stop-orders: [0]\n")
2279
2280            if details in ["full", "analytics"]:
2281                # -- Show analytics section:
2282                if view["stat"]["portfolioCostRUB"] > 0:
2283                    info.extend([
2284                        "\n# Analytics\n\n"
2285                        "* **Actual on date:** [{} UTC]\n".format(actualOnDate),
2286                        "* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]),
2287                        "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format(
2288                            "+" if view["stat"]["totalChangesRUB"] > 0 else "",
2289                            view["stat"]["totalChangesRUB"],
2290                            "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "",
2291                            view["stat"]["totalChangesPercentRUB"],
2292                        ),
2293                        "\n## Portfolio distribution by assets\n"
2294                        "\n| Type                               | Uniques | Percent | Current cost       |\n",
2295                        "|------------------------------------|---------|---------|--------------------|\n",
2296                    ])
2297
2298                    for key in view["analytics"]["distrByAssets"].keys():
2299                        if view["analytics"]["distrByAssets"][key]["cost"] > 0:
2300                            info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format(
2301                                key,
2302                                view["analytics"]["distrByAssets"][key]["uniques"],
2303                                "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]),
2304                                "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]),
2305                            ))
2306
2307                    aSepLine = "|----------------------------------------------|---------|--------------------|\n"
2308
2309                    info.extend([
2310                        "\n## Portfolio distribution by companies\n"
2311                        "\n| Company                                      | Percent | Current cost       |\n",
2312                        aSepLine,
2313                    ])
2314
2315                    for company in view["analytics"]["distrByCompanies"].keys():
2316                        if view["analytics"]["distrByCompanies"][company]["cost"] > 0:
2317                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2318                                "{}{}".format(
2319                                    "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "",
2320                                    company,
2321                                ),
2322                                "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]),
2323                                "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]),
2324                            ))
2325
2326                    info.extend([
2327                        "\n## Portfolio distribution by sectors\n"
2328                        "\n| Sector                                       | Percent | Current cost       |\n",
2329                        aSepLine,
2330                    ])
2331
2332                    for sector in view["analytics"]["distrBySectors"].keys():
2333                        if view["analytics"]["distrBySectors"][sector]["cost"] > 0:
2334                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2335                                sector,
2336                                "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]),
2337                                "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]),
2338                            ))
2339
2340                    info.extend([
2341                        "\n## Portfolio distribution by currencies\n"
2342                        "\n| Instruments currencies                       | Percent | Current cost       |\n",
2343                        aSepLine,
2344                    ])
2345
2346                    for curr in view["analytics"]["distrByCurrencies"].keys():
2347                        if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0:
2348                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2349                                "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]),
2350                                "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]),
2351                                "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]),
2352                            ))
2353
2354                    info.extend([
2355                        "\n## Portfolio distribution by countries\n"
2356                        "\n| Assets by country                            | Percent | Current cost       |\n",
2357                        aSepLine,
2358                    ])
2359
2360                    for country in view["analytics"]["distrByCountries"].keys():
2361                        if view["analytics"]["distrByCountries"][country]["cost"] > 0:
2362                            info.append("| {:<44} | {:<7} | {:<18} |\n".format(
2363                                country,
2364                                "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]),
2365                                "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]),
2366                            ))
2367
2368            if details in ["full", "calendar"]:
2369                # -- Show bonds payment calendar section:
2370                if view["stat"]["Bonds"]:
2371                    bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]]
2372                    view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False)
2373                    info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False))
2374
2375                else:
2376                    info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n")
2377
2378            infoText = "".join(info)
2379
2380            uLogger.info(infoText)
2381
2382            if details == "full" and self.overviewFile:
2383                filename = self.overviewFile
2384
2385            elif details == "digest" and self.overviewDigestFile:
2386                filename = self.overviewDigestFile
2387
2388            elif details == "positions" and self.overviewPositionsFile:
2389                filename = self.overviewPositionsFile
2390
2391            elif details == "orders" and self.overviewOrdersFile:
2392                filename = self.overviewOrdersFile
2393
2394            elif details == "analytics" and self.overviewAnalyticsFile:
2395                filename = self.overviewAnalyticsFile
2396
2397            elif details == "calendar" and self.overviewBondsCalendarFile:
2398                filename = self.overviewBondsCalendarFile
2399
2400            else:
2401                filename = ""
2402
2403            if filename:
2404                with open(filename, "w", encoding="UTF-8") as fH:
2405                    fH.write(infoText)
2406
2407                uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename)))
2408
2409                if self.useHTMLReports:
2410                    htmlFilePath = filename.replace(".md", ".html") if filename.endswith(".md") else filename + ".html"
2411                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2412                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's portfolio", commonCSS=COMMON_CSS, markdown=infoText))
2413
2414                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2415
2416        return view

Get portfolio: all open positions, orders and some statistics for current accountId. If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile and overviewBondsCalendarFile are defined then also save information to file.

WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.

Parameters
  • show: if False then only dictionary returns, if True then show more debug information.
  • details: how detailed should the information be?
    • full — shows full available information about portfolio status (by default),
    • positions — shows only open positions,
    • orders — shows only sections of open limits and stop orders.
    • digest — show a short digest of the portfolio status,
    • analytics — shows only the analytics section and the distribution of the portfolio by various categories,
    • calendar — shows only the bonds calendar section (if these present in portfolio),
Returns

dictionary with client's raw portfolio and some statistics.

def Deals( self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2418    def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]:
2419        """
2420        Returns history operations between two given dates for current `accountId`.
2421        If `reportFile` string is not empty then also save human-readable report.
2422        Shows some statistical data of closed positions.
2423
2424        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2425        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2426        :param show: if `True` then also prints all records to the console.
2427        :param showCancelled: if `False` then remove information about cancelled operations from the deals report.
2428        :return: original list of dictionaries with history of deals records from API ("operations" key):
2429                 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2430                 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2431        """
2432        if self.accountId is None or not self.accountId:
2433            uLogger.error("Variable `accountId` must be defined for using this method!")
2434            raise Exception("Account ID required")
2435
2436        startDate, endDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2437
2438        uLogger.debug("Requesting history of a client's operations. Wait, please...")
2439
2440        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations
2441        dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations"
2442        self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate})
2443        ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"]  # list of dict: operations returns by broker
2444        customStat = {}  # custom statistics in additional to responseJSON
2445
2446        # --- output report in human-readable format:
2447        if show or self.reportFile:
2448            splitLine1 = "|                            |                               |                              |                      |                        |\n"  # Summary section
2449            splitLine2 = "|                     |              |              |            |           |                 |            |                                                                    |\n"  # Operations section
2450            nextDay = ""
2451
2452            info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])]
2453
2454            if len(ops) > 0:
2455                customStat = {
2456                    "opsCount": 0,  # total operations count
2457                    "buyCount": 0,  # buy operations
2458                    "sellCount": 0,  # sell operations
2459                    "buyTotal": {"rub": 0.},  # Buy sums in different currencies
2460                    "sellTotal": {"rub": 0.},  # Sell sums in different currencies
2461                    "payIn": {"rub": 0.},  # Deposit brokerage account
2462                    "payOut": {"rub": 0.},  # Withdrawals
2463                    "divs": {"rub": 0.},  # Dividends income
2464                    "coupons": {"rub": 0.},  # Coupon's income
2465                    "brokerCom": {"rub": 0.},  # Service commissions
2466                    "serviceCom": {"rub": 0.},  # Service commissions
2467                    "marginCom": {"rub": 0.},  # Margin commissions
2468                    "allTaxes": {"rub": 0.},  # Sum of withholding taxes and corrections
2469                }
2470
2471                # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES:
2472                for item in ops:
2473                    if item["state"] == "OPERATION_STATE_EXECUTED":
2474                        payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2475
2476                        # count buy operations:
2477                        if "_BUY" in item["operationType"]:
2478                            customStat["buyCount"] += 1
2479
2480                            if item["payment"]["currency"] in customStat["buyTotal"].keys():
2481                                customStat["buyTotal"][item["payment"]["currency"]] += payment
2482
2483                            else:
2484                                customStat["buyTotal"][item["payment"]["currency"]] = payment
2485
2486                        # count sell operations:
2487                        elif "_SELL" in item["operationType"]:
2488                            customStat["sellCount"] += 1
2489
2490                            if item["payment"]["currency"] in customStat["sellTotal"].keys():
2491                                customStat["sellTotal"][item["payment"]["currency"]] += payment
2492
2493                            else:
2494                                customStat["sellTotal"][item["payment"]["currency"]] = payment
2495
2496                        # count incoming operations:
2497                        elif item["operationType"] in ["OPERATION_TYPE_INPUT"]:
2498                            if item["payment"]["currency"] in customStat["payIn"].keys():
2499                                customStat["payIn"][item["payment"]["currency"]] += payment
2500
2501                            else:
2502                                customStat["payIn"][item["payment"]["currency"]] = payment
2503
2504                        # count withdrawals operations:
2505                        elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]:
2506                            if item["payment"]["currency"] in customStat["payOut"].keys():
2507                                customStat["payOut"][item["payment"]["currency"]] += payment
2508
2509                            else:
2510                                customStat["payOut"][item["payment"]["currency"]] = payment
2511
2512                        # count dividends income:
2513                        elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]:
2514                            if item["payment"]["currency"] in customStat["divs"].keys():
2515                                customStat["divs"][item["payment"]["currency"]] += payment
2516
2517                            else:
2518                                customStat["divs"][item["payment"]["currency"]] = payment
2519
2520                        # count coupon's income:
2521                        elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]:
2522                            if item["payment"]["currency"] in customStat["coupons"].keys():
2523                                customStat["coupons"][item["payment"]["currency"]] += payment
2524
2525                            else:
2526                                customStat["coupons"][item["payment"]["currency"]] = payment
2527
2528                        # count broker commissions:
2529                        elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]:
2530                            if item["payment"]["currency"] in customStat["brokerCom"].keys():
2531                                customStat["brokerCom"][item["payment"]["currency"]] += payment
2532
2533                            else:
2534                                customStat["brokerCom"][item["payment"]["currency"]] = payment
2535
2536                        # count service commissions:
2537                        elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]:
2538                            if item["payment"]["currency"] in customStat["serviceCom"].keys():
2539                                customStat["serviceCom"][item["payment"]["currency"]] += payment
2540
2541                            else:
2542                                customStat["serviceCom"][item["payment"]["currency"]] = payment
2543
2544                        # count margin commissions:
2545                        elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]:
2546                            if item["payment"]["currency"] in customStat["marginCom"].keys():
2547                                customStat["marginCom"][item["payment"]["currency"]] += payment
2548
2549                            else:
2550                                customStat["marginCom"][item["payment"]["currency"]] = payment
2551
2552                        # count withholding taxes:
2553                        elif "_TAX" in item["operationType"]:
2554                            if item["payment"]["currency"] in customStat["allTaxes"].keys():
2555                                customStat["allTaxes"][item["payment"]["currency"]] += payment
2556
2557                            else:
2558                                customStat["allTaxes"][item["payment"]["currency"]] = payment
2559
2560                        else:
2561                            continue
2562
2563                customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"]
2564
2565                # --- view "Actions" lines:
2566                info.extend([
2567                    "| Report sections            |                               |                              |                      |                        |\n",
2568                    "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n",
2569                    "| **Actions:**               | Trades: {:<21} | Trading volumes:             |                      |                        |\n".format(customStat["opsCount"]),
2570                    "|                            |   Buy: {:<22} | {:<28} |                      |                        |\n".format(
2571                        "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2572                        "  rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else "  —",
2573                    ),
2574                    "|                            |   Sell: {:<21} | {:<28} |                      |                        |\n".format(
2575                        "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0,
2576                        "  rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else "  —",
2577                    ),
2578                ])
2579
2580                opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys()))))
2581                for key in opsKeys:
2582                    if key == "rub":
2583                        continue
2584
2585                    info.extend([
2586                        "|                            |                               | {:<28} |                      |                        |\n".format(
2587                            "  {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0)
2588                        ),
2589                        "|                            |                               | {:<28} |                      |                        |\n".format(
2590                            "  {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0)
2591                        ),
2592                    ])
2593
2594                info.append(splitLine1)
2595
2596                def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str:
2597                    return "|                            | {:<29} | {:<28} | {:<20} | {:<22} |\n".format(
2598                            "  {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else "  —",
2599                            "  {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else "  —",
2600                            "  {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else "  —",
2601                            "  {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else "  —",
2602                    )
2603
2604                # --- view "Payments" lines:
2605                info.append("| **Payments:**              | Deposit on broker account:    | Withdrawals:                 | Dividends income:    | Coupons income:        |\n")
2606                paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys()))))
2607
2608                for key in paymentsKeys:
2609                    info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key))
2610
2611                info.append(splitLine1)
2612
2613                # --- view "Commissions and taxes" lines:
2614                info.append("| **Commissions and taxes:** | Broker commissions:           | Service commissions:         | Margin commissions:  | All taxes/corrections: |\n")
2615                comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys()))))
2616
2617                for key in comKeys:
2618                    info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key))
2619
2620                info.extend([
2621                    "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"),
2622                    "| Date and time       | FIGI         | Ticker       | Asset      | Value     | Payment         | Status     | Operation type                                                     |\n",
2623                    "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n",
2624                ])
2625
2626            else:
2627                info.append("Broker returned no operations during this period\n")
2628
2629            # --- view "Operations" section:
2630            for item in ops:
2631                if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]:
2632                    continue
2633
2634                else:
2635                    self._figi = item["figi"]
2636                    payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"])
2637                    instrument = self.SearchByFIGI(requestPrice=False) if self._figi else {}
2638
2639                    # group of deals during one day:
2640                    if nextDay and item["date"].split("T")[0] != nextDay:
2641                        info.append(splitLine2)
2642                        nextDay = ""
2643
2644                    else:
2645                        nextDay = item["date"].split("T")[0]  # saving current day for splitting
2646
2647                    info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format(
2648                        item["date"].replace("T", " ").replace("Z", "").split(".")[0],
2649                        self._figi if self._figi else "—",
2650                        instrument["ticker"] if instrument else "—",
2651                        instrument["type"] if instrument else "—",
2652                        item["quantity"] if int(item["quantity"]) > 0 else "—",
2653                        "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—",
2654                        TKS_OPERATION_STATES[item["state"]],
2655                        TKS_OPERATION_TYPES[item["operationType"]],
2656                    ))
2657
2658            infoText = "".join(info)
2659
2660            if show:
2661                if self.moreDebug:
2662                    uLogger.debug("Records about history of a client's operations successfully received")
2663
2664                uLogger.info(infoText)
2665
2666            if self.reportFile:
2667                with open(self.reportFile, "w", encoding="UTF-8") as fH:
2668                    fH.write(infoText)
2669
2670                uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile)))
2671
2672                if self.useHTMLReports:
2673                    htmlFilePath = self.reportFile.replace(".md", ".html") if self.reportFile.endswith(".md") else self.reportFile + ".html"
2674                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
2675                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Client's operations", commonCSS=COMMON_CSS, markdown=infoText))
2676
2677                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
2678
2679        return ops, customStat

Returns history operations between two given dates for current accountId. If reportFile string is not empty then also save human-readable report. Shows some statistical data of closed positions.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • show: if True then also prints all records to the console.
  • showCancelled: if False then remove information about cancelled operations from the deals report.
Returns

original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.

def History( self, start: str = None, end: str = None, interval: str = 'hour', onlyMissing: bool = False, csvSep: str = ',', show: bool = False) -> pandas.core.frame.DataFrame:
2681    def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame:
2682        """
2683        This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id).
2684
2685        History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`.
2686        Warning! Broker server used ISO UTC time by default.
2687
2688        If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame.
2689        Also, `historyFile` used to update history with `onlyMissing` parameter.
2690
2691        See also: `LoadHistory()` and `ShowHistoryChart()` methods.
2692
2693        :param start: see docstring in `TradeRoutines.GetDatesAsString()` method.
2694        :param end: see docstring in `TradeRoutines.GetDatesAsString()` method.
2695        :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`,
2696                         `"hour"`, `"day"`. Default: `"hour"`.
2697        :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`.
2698                            False by default. Warning! History appends only from last candle to current time
2699                            with always update last candle!
2700        :param csvSep: separator if csv-file is used, `,` by default.
2701        :param show: if `True` then also prints Pandas DataFrame to the console.
2702        :return: Pandas DataFrame with prices history. Headers of columns are defined by default:
2703                 `["date", "time", "open", "high", "low", "close", "volume"]`.
2704        """
2705        strStartDate, strEndDate = GetDatesAsString(start, end, userFormat=TKS_DATE_FORMAT, outputFormat=TKS_DATE_TIME_FORMAT)  # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z")
2706        headers = ["date", "time", "open", "high", "low", "close", "volume"]  # sequence and names of column headers
2707        history = None  # empty pandas object for history
2708
2709        if interval not in TKS_CANDLE_INTERVALS.keys():
2710            uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.")
2711            raise Exception("Incorrect value")
2712
2713        if not (self._ticker or self._figi):
2714            uLogger.error("Ticker or FIGI must be defined!")
2715            raise Exception("Ticker or FIGI required")
2716
2717        if self._ticker and not self._figi:
2718            instrumentByTicker = self.SearchByTicker(requestPrice=False)
2719            self._figi = instrumentByTicker["figi"] if instrumentByTicker else ""
2720
2721        if self._figi and not self._ticker:
2722            instrumentByFIGI = self.SearchByFIGI(requestPrice=False)
2723            self._ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else ""
2724
2725        dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from start time string
2726        dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc())  # datetime object from end time string
2727        if interval.lower() != "day":
2728            dtEnd += timedelta(seconds=1)  # adds 1 sec for requests, because day end returned by `TradeRoutines.GetDatesAsString()` is 23:59:59
2729
2730        delta = dtEnd - dtStart  # current UTC time minus last time in file
2731        deltaMinutes = delta.days * 1440 + delta.seconds // 60  # minutes between start and end dates
2732
2733        # calculate history length in candles:
2734        length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1]
2735        if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0:
2736            length += 1  # to avoid fraction time
2737
2738        # calculate data blocks count:
2739        blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2]
2740
2741        uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end))
2742        uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate))
2743        uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval))
2744        uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2]))
2745        uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self._ticker, self._figi))
2746
2747        tempOld = None  # pandas object for old history, if --only-missing key present
2748        lastTime = None  # datetime object of last old candle in file
2749
2750        if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile):
2751            uLogger.debug("--only-missing key present, add only last missing candles...")
2752            uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile)))
2753
2754            tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers)
2755
2756            tempOld["date"] = pd.to_datetime(tempOld["date"])  # load date "as is"
2757            tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d")  # convert date to string
2758            tempOld["time"] = pd.to_datetime(tempOld["time"])  # load time "as is"
2759            tempOld["time"] = tempOld["time"].dt.strftime("%H:%M")  # convert time to string
2760
2761            # get last datetime object from last string in file or minus 1 delta if file is empty:
2762            if len(tempOld) > 0:
2763                lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2764
2765            else:
2766                lastTime = dtEnd - timedelta(days=1)  # history file is empty, so last date set at -1 day
2767
2768            tempOld = tempOld[:-1]  # always remove last old candle because it may be incompletely at the current time
2769
2770        responseJSONs = []  # raw history blocks of data
2771
2772        blockEnd = dtEnd
2773        for item in range(blocks):
2774            tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2]
2775            blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail)
2776
2777            uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format(
2778                item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2779            ))
2780
2781            if blockStart == blockEnd:
2782                uLogger.debug("Skipped this zero-length block...")
2783
2784            else:
2785                # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles
2786                historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles"
2787                self.body = str({
2788                    "figi": self._figi,
2789                    "from": blockStart.strftime(TKS_DATE_TIME_FORMAT),
2790                    "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT),
2791                    "interval": TKS_CANDLE_INTERVALS[interval][0]
2792                })
2793                responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1)
2794
2795                if "code" in responseJSON.keys():
2796                    uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks))
2797
2798                else:
2799                    if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1:
2800                        responseJSON["candles"] = responseJSON["candles"][:-1]  # removes last candle for "yesterday" request
2801
2802                    responseJSONs = responseJSON["candles"] + responseJSONs  # add more old history behind newest dates
2803
2804            blockEnd = blockStart
2805
2806        printCount = len(responseJSONs)  # candles to show in console
2807        if responseJSONs:
2808            tempHistory = pd.DataFrame(
2809                data={
2810                    "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2811                    "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs],
2812                    "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs],
2813                    "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs],
2814                    "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs],
2815                    "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs],
2816                    "volume": [int(item["volume"]) for item in responseJSONs],
2817                },
2818                index=range(len(responseJSONs)),
2819                columns=["date", "time", "open", "high", "low", "close", "volume"],
2820            )
2821            tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d")
2822            tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M")
2823
2824            # append only newest candles to old history if --only-missing key present:
2825            if onlyMissing and tempOld is not None and lastTime is not None:
2826                index = 0  # find start index in tempHistory data:
2827
2828                for i, item in tempHistory.iterrows():
2829                    curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc())
2830
2831                    if curTime == lastTime:
2832                        uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT)))
2833                        index = i
2834                        printCount = index + 1
2835                        break
2836
2837                history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True)
2838
2839            else:
2840                history = tempHistory  # if no `--only-missing` key then load full data from server
2841
2842            uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False)))
2843
2844        if history is not None and not history.empty:
2845            if show:
2846                uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format(
2847                    strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]),
2848                    pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False),
2849                ))
2850
2851        else:
2852            uLogger.warning("Received an empty candles history!")
2853
2854        if self.historyFile is not None:
2855            if history is not None and not history.empty:
2856                history.to_csv(self.historyFile, sep=csvSep, index=False, header=None)
2857                uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self._ticker, self._figi, interval, os.path.abspath(self.historyFile)))
2858
2859            else:
2860                uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile)))
2861
2862        else:
2863            uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.")
2864
2865        return history

This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).

History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01. Warning! Broker server used ISO UTC time by default.

If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame. Also, historyFile used to update history with onlyMissing parameter.

See also: LoadHistory() and ShowHistoryChart() methods.

Parameters
  • start: see docstring in TradeRoutines.GetDatesAsString() method.
  • end: see docstring in TradeRoutines.GetDatesAsString() method.
  • interval: this is a candle interval. Current available values are "1min", "5min", "15min", "hour", "day". Default: "hour".
  • onlyMissing: if True then add only last missing candles, do not request all history length from start. False by default. Warning! History appends only from last candle to current time with always update last candle!
  • csvSep: separator if csv-file is used, , by default.
  • show: if True then also prints Pandas DataFrame to the console.
Returns

Pandas DataFrame with prices history. Headers of columns are defined by default: ["date", "time", "open", "high", "low", "close", "volume"].

def LoadHistory(self, filePath: str) -> pandas.core.frame.DataFrame:
2867    def LoadHistory(self, filePath: str) -> pd.DataFrame:
2868        """
2869        Load candles history from csv-file and return Pandas DataFrame object.
2870
2871        See also: `History()` and `ShowHistoryChart()` methods.
2872
2873        :param filePath: path to csv-file to open.
2874        """
2875        loadedHistory = None  # init candles data object
2876
2877        uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...")
2878
2879        if os.path.exists(filePath):
2880            loadedHistory = self.priceModel.LoadFromFile(filePath)  # load data and get chain of candles as Pandas DataFrame
2881
2882            tfStr = self.priceModel.FormattedDelta(
2883                self.priceModel.timeframe,
2884                "{days} days {hours}h {minutes}m {seconds}s",
2885            ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta(
2886                self.priceModel.timeframe,
2887                "{hours}h {minutes}m {seconds}s",
2888            )
2889
2890            if loadedHistory is not None and not loadedHistory.empty:
2891                uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format(
2892                    len(loadedHistory),
2893                    tfStr,
2894                    pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)),
2895                )
2896
2897            else:
2898                uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath)))
2899
2900        else:
2901            uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath))
2902
2903        return loadedHistory

Load candles history from csv-file and return Pandas DataFrame object.

See also: History() and ShowHistoryChart() methods.

Parameters
  • filePath: path to csv-file to open.
def ShowHistoryChart( self, candles: Union[str, pandas.core.frame.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2905    def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None:
2906        """
2907        Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
2908
2909        Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart.
2910        Default: `index.html` (both for interact and non-interact candlesticks chart).
2911
2912        See also: `History()` and `LoadHistory()` methods.
2913
2914        :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
2915        :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart.
2916                         See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters
2917                         If False then chain of candlesticks will render as not interactive Google Candlestick chart.
2918                         See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
2919        :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to
2920                              html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file.
2921        """
2922        if isinstance(candles, str):
2923            self.priceModel.prices = self.LoadHistory(filePath=candles)  # load candles chain from file
2924            self.priceModel.ticker = os.path.basename(candles)  # use filename as ticker name in PriceGenerator
2925
2926        elif isinstance(candles, pd.DataFrame):
2927            self.priceModel.prices = candles  # set candles chain from variable
2928            self.priceModel.ticker = self._ticker  # use current TKSBrokerAPI ticker as ticker name in PriceGenerator
2929
2930            if "datetime" not in candles.columns:
2931                self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True)  # PriceGenerator uses "datetime" column with date and time
2932
2933        else:
2934            uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!")
2935            raise Exception("Incorrect value")
2936
2937        self.priceModel.horizon = len(self.priceModel.prices)  # use length of candles data as horizon in PriceGenerator
2938
2939        if interact:
2940            uLogger.debug("Rendering interactive candles chart. Wait, please...")
2941
2942            self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2943
2944        else:
2945            uLogger.debug("Rendering non-interactive candles chart. Wait, please...")
2946
2947            self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser)
2948
2949        uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))

Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.

Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart. Default: index.html (both for interact and non-interact candlesticks chart).

See also: History() and LoadHistory() methods.

Parameters
def Trade( self, operation: str, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
2951    def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
2952        """
2953        Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response.
2954        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
2955
2956        See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`.
2957
2958        :param operation: string "Buy" or "Sell".
2959        :param lots: volume, integer count of lots >= 1.
2960        :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`.
2961        :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`.
2962        :param expDate: string "Undefined" by default or local date in future,
2963                        it is a string with format `%Y-%m-%d %H:%M:%S`.
2964        :return: JSON with response from broker server.
2965        """
2966        if self.accountId is None or not self.accountId:
2967            uLogger.error("Variable `accountId` must be defined for using this method!")
2968            raise Exception("Account ID required")
2969
2970        if operation is None or not operation or operation not in ("Buy", "Sell"):
2971            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
2972            raise Exception("Incorrect value")
2973
2974        if lots is None or lots < 1:
2975            uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.")
2976            lots = 1
2977
2978        if tp is None or tp < 0:
2979            tp = 0
2980
2981        if sl is None or sl < 0:
2982            sl = 0
2983
2984        if expDate is None or not expDate:
2985            expDate = "Undefined"
2986
2987        if not (self._ticker or self._figi):
2988            uLogger.error("Ticker or FIGI must be defined!")
2989            raise Exception("Ticker or FIGI required")
2990
2991        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
2992        self._ticker = instrument["ticker"]
2993        self._figi = instrument["figi"]
2994
2995        uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self._ticker, self._figi, lots, tp, sl, expDate))
2996
2997        openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
2998        self.body = str({
2999            "figi": self._figi,
3000            "quantity": str(lots),
3001            "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3002            "accountId": str(self.accountId),
3003            "orderType": "ORDER_TYPE_MARKET",  # see: TKS_ORDER_TYPES
3004        })
3005        response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0)
3006
3007        if "orderId" in response.keys():
3008            uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format(
3009                operation, response["orderId"],
3010                self._ticker, self._figi, lots,
3011                NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"],
3012                NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"],
3013                NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"],
3014            ))
3015
3016            if tp > 0:
3017                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate)
3018
3019            if sl > 0:
3020                self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate)
3021
3022        else:
3023            uLogger.warning("Not `oK` status received! Market order not executed. See full debug log and try again open order later.")
3024
3025        return response

Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().

Parameters
  • operation: string "Buy" or "Sell".
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter targetPrice in self.Order().
  • sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter targetPrice in self.Order().
  • expDate: string "Undefined" by default or local date in future, it is a string with format %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Buy( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3027    def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3028        """
3029        More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response.
3030        If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter.
3031
3032        See also: `Order()` and `Trade()` docstrings.
3033
3034        :param lots: volume, integer count of lots >= 1.
3035        :param tp: float > 0, take profit price of stop-order.
3036        :param sl: float > 0, stop loss price of stop-order.
3037        :param expDate: it's a local date in future.
3038                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3039        :return: JSON with response from broker server.
3040        """
3041        return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def Sell( self, lots: int = 1, tp: float = 0.0, sl: float = 0.0, expDate: str = 'Undefined') -> dict:
3043    def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict:
3044        """
3045        More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response.
3046        If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter.
3047
3048        See also: `Order()` and `Trade()` docstrings.
3049
3050        :param lots: volume, integer count of lots >= 1.
3051        :param tp: float > 0, take profit price of stop-order.
3052        :param sl: float > 0, stop loss price of stop-order.
3053        :param expDate: it's a local date in the future.
3054                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3055        :return: JSON with response from broker server.
3056        """
3057        return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)

More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response. If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.

See also: Order() and Trade() docstrings.

Parameters
  • lots: volume, integer count of lots >= 1.
  • tp: float > 0, take profit price of stop-order.
  • sl: float > 0, stop loss price of stop-order.
  • expDate: it's a local date in the future. String has a format like this: %Y-%m-%d %H:%M:%S.
Returns

JSON with response from broker server.

def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3059    def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None:
3060        """
3061        Close position of given instruments.
3062
3063        :param instruments: list of instruments defined by tickers or FIGIs that must be closed.
3064        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3065                         This avoids unnecessary downloading data from the server.
3066        """
3067        if instruments is None or not instruments:
3068            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
3069            raise Exception("Ticker or FIGI required")
3070
3071        if isinstance(instruments, str):
3072            instruments = [instruments]
3073
3074        uniqueInstruments = self.GetUniqueFIGIs(instruments)
3075        if uniqueInstruments:
3076            if portfolio is None or not portfolio:
3077                portfolio = self.Overview(show=False)
3078
3079            allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]]
3080            uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened)))
3081
3082            for self._figi in uniqueInstruments:
3083                if self._figi not in allOpened:
3084                    uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self._figi))
3085                    continue
3086
3087                # search open trade info about instrument by ticker:
3088                instrument = {}
3089                for iType in TKS_INSTRUMENTS:
3090                    if instrument:
3091                        break
3092
3093                    for item in portfolio["stat"][iType]:
3094                        if item["figi"] == self._figi:
3095                            instrument = item
3096                            break
3097
3098                if instrument:
3099                    self._ticker = instrument["ticker"]
3100                    self._figi = instrument["figi"]
3101
3102                    uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format(
3103                        self._ticker,
3104                        self._figi,
3105                        int(instrument["volume"]),
3106                        ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "",
3107                    ))
3108
3109                    tradeLots = abs(instrument["lots"]) - instrument["blocked"]  # available volumes in lots for close operation
3110
3111                    if tradeLots > 0:
3112                        if instrument["blocked"] > 0:
3113                            uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format(
3114                                instrument["blocked"],
3115                                self._ticker,
3116                                tradeLots,
3117                            ))
3118
3119                        # if direction is "Long" then we need sell, if direction is "Short" then we need buy:
3120                        self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots)
3121
3122                    else:
3123                        uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self._ticker))

Close position of given instruments.

Parameters
  • instruments: list of instruments defined by tickers or FIGIs that must be closed.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3125    def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None:
3126        """
3127        Close all positions of given instruments with defined type.
3128
3129        :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
3130        :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method.
3131                         This avoids unnecessary downloading data from the server.
3132        """
3133        if iType not in TKS_INSTRUMENTS:
3134            uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType))
3135
3136        else:
3137            if portfolio is None or not portfolio:
3138                portfolio = self.Overview(show=False)
3139
3140            tickers = [item["ticker"] for item in portfolio["stat"][iType]]
3141            uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers))
3142
3143            if tickers and portfolio:
3144                self.CloseTrades(tickers, portfolio)
3145
3146            else:
3147                uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))

Close all positions of given instruments with defined type.

Parameters
  • iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
  • portfolio: pre-received dictionary with open trades, returned by Overview() method. This avoids unnecessary downloading data from the server.
def Order( self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3149    def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3150        """
3151        Universal method to create market or limit orders with all available parameters for current `accountId`.
3152        See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`.
3153
3154        If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above
3155        current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
3156
3157        Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell"
3158        then broker immediately open market order as you can do simple --buy or --sell operations!
3159
3160        If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell".
3161        When current price will go up or down to target price value then broker opens a limit order.
3162        Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
3163
3164        Only one attempt and no retry for opens order. If network issue occurred you can create new request.
3165
3166        :param operation: string "Buy" or "Sell".
3167        :param orderType: string "Limit" or "Stop".
3168        :param lots: volume, integer count of lots >= 1.
3169        :param targetPrice: target price > 0. This is open trade price for limit order.
3170        :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice.
3171                           Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
3172        :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types
3173                         "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3174                         Stop loss order always executed by market price.
3175        :param expDate: string "Undefined" by default or local date in future.
3176                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3177                        This date is converting to UTC format for server. This parameter only makes sense for stop-order.
3178                        A limit order has no expiration date, it lasts until the end of the trading day.
3179        :return: JSON with response from broker server.
3180        """
3181        if self.accountId is None or not self.accountId:
3182            uLogger.error("Variable `accountId` must be defined for using this method!")
3183            raise Exception("Account ID required")
3184
3185        if operation is None or not operation or operation not in ("Buy", "Sell"):
3186            uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!")
3187            raise Exception("Incorrect value")
3188
3189        if orderType is None or not orderType or orderType not in ("Limit", "Stop"):
3190            uLogger.error("You must define order type only one of them: `Limit` or `Stop`!")
3191            raise Exception("Incorrect value")
3192
3193        if lots is None or lots < 1:
3194            uLogger.error("You must define trade volume > 0: integer count of lots!")
3195            raise Exception("Incorrect value")
3196
3197        if targetPrice is None or targetPrice <= 0:
3198            uLogger.error("Target price for limit-order must be greater than 0!")
3199            raise Exception("Incorrect value")
3200
3201        if limitPrice is None or limitPrice <= 0:
3202            limitPrice = targetPrice
3203
3204        if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"):
3205            stopType = "Limit"
3206
3207        if expDate is None or not expDate:
3208            expDate = "Undefined"
3209
3210        if not (self._ticker or self._figi):
3211            uLogger.error("Tocker or FIGI must be defined!")
3212            raise Exception("Ticker or FIGI required")
3213
3214        response = {}
3215        instrument = self.SearchByTicker(requestPrice=True) if self._ticker else self.SearchByFIGI(requestPrice=True)
3216        self._ticker = instrument["ticker"]
3217        self._figi = instrument["figi"]
3218
3219        if orderType == "Limit":
3220            uLogger.debug(
3221                "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format(
3222                    self._ticker, self._figi,
3223                    operation, lots, targetPrice, instrument["currency"],
3224                ))
3225
3226            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder"
3227            self.body = str({
3228                "figi": self._figi,
3229                "quantity": str(lots),
3230                "price": FloatToNano(targetPrice),
3231                "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL",  # see: TKS_ORDER_DIRECTIONS
3232                "accountId": str(self.accountId),
3233                "orderType": "ORDER_TYPE_LIMIT",  # see: TKS_ORDER_TYPES
3234            })
3235            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3236
3237            if "orderId" in response.keys():
3238                uLogger.info(
3239                    "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}]".format(
3240                        response["orderId"], self._ticker, self._figi, operation, lots,
3241                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3242                    ))
3243
3244                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3245                    if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]:
3246                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format(
3247                            targetPrice, instrument["currency"],
3248                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3249                        ))
3250
3251                    if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]:
3252                        uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format(
3253                            targetPrice, instrument["currency"],
3254                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3255                        ))
3256
3257            else:
3258                uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log and try again open order later.")
3259
3260        if orderType == "Stop":
3261            uLogger.debug(
3262                "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format(
3263                    self._ticker, self._figi,
3264                    operation, lots,
3265                    targetPrice, instrument["currency"],
3266                    limitPrice, instrument["currency"],
3267                    stopType, expDate,
3268                ))
3269
3270            openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder"
3271            expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT)
3272            stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT"
3273
3274            body = {
3275                "figi": self._figi,
3276                "quantity": str(lots),
3277                "price": FloatToNano(limitPrice),
3278                "stopPrice": FloatToNano(targetPrice),
3279                "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL",  # see: TKS_STOP_ORDER_DIRECTIONS
3280                "accountId": str(self.accountId),
3281                "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL",  # see: TKS_STOP_ORDER_EXPIRATION_TYPES
3282                "stopOrderType": stopOrderType,  # see: TKS_STOP_ORDER_TYPES
3283            }
3284
3285            if expDateUTC:
3286                body["expireDate"] = expDateUTC
3287
3288            self.body = str(body)
3289            response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0)
3290
3291            if "stopOrderId" in response.keys():
3292                uLogger.info(
3293                    "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{} {}], limit price [{} {}], stop-order type [{}] and expiration date [{} UTC]".format(
3294                        response["stopOrderId"], self._ticker, self._figi, operation, lots,
3295                        "{:.4f}".format(targetPrice).rstrip("0").rstrip("."), instrument["currency"],
3296                        "{:.4f}".format(limitPrice).rstrip("0").rstrip("."), instrument["currency"],
3297                        TKS_STOP_ORDER_TYPES[stopOrderType],
3298                        datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"],
3299                    ))
3300
3301                if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]:
3302                    if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3303                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3304                            targetPrice, instrument["currency"],
3305                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3306                        ))
3307
3308                    if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP":
3309                        uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format(
3310                            targetPrice, instrument["currency"],
3311                            instrument["currentPrice"]["lastPrice"], instrument["currency"],
3312                        ))
3313
3314            else:
3315                uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log and try again open order later.")
3316
3317        return response

Universal method to create market or limit orders with all available parameters for current accountId. See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().

If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.

Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!

If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.

Only one attempt and no retry for opens order. If network issue occurred you can create new request.

Parameters
  • operation: string "Buy" or "Sell".
  • orderType: string "Limit" or "Stop".
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
  • limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
  • stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns

JSON with response from broker server.

def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3319    def BuyLimit(self, lots: int, targetPrice: float) -> dict:
3320        """
3321        Create pending `Buy` limit-order (below current price). You must specify only 2 parameters:
3322        `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then
3323        broker immediately open `Buy` market order, such as if you do simple `--buy` operation!
3324        See also: `Order()` docstring.
3325
3326        :param lots: volume, integer count of lots >= 1.
3327        :param targetPrice: target price > 0. This is open trade price for limit order.
3328        :return: JSON with response from broker server.
3329        """
3330        return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Buy limit-order (below current price). You must specify only 2 parameters: lots and target price to open buy limit-order. If you try to create buy limit-order above current price then broker immediately open Buy market order, such as if you do simple --buy operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def BuyStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3332    def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3333        """
3334        Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order.
3335        In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3336        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3337        target price value then broker opens a limit order. See also: `Order()` docstring.
3338
3339        :param lots: volume, integer count of lots >= 1.
3340        :param targetPrice: target price > 0. This is trigger price for buy stop-order.
3341        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3342                           with price equal to limitPrice, when current price goes to target price of buy stop-order.
3343        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3344                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3345        :param expDate: string "Undefined" by default or local date in future.
3346                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3347                        This date is converting to UTC format for server.
3348        :return: JSON with response from broker server.
3349        """
3350        return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order. In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for buy stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def SellLimit(self, lots: int, targetPrice: float) -> dict:
3352    def SellLimit(self, lots: int, targetPrice: float) -> dict:
3353        """
3354        Create pending `Sell` limit-order (above current price). You must specify only 2 parameters:
3355        `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then
3356        broker immediately open `Sell` market order, such as if you do simple `--sell` operation!
3357        See also: `Order()` docstring.
3358
3359        :param lots: volume, integer count of lots >= 1.
3360        :param targetPrice: target price > 0. This is open trade price for limit order.
3361        :return: JSON with response from broker server.
3362        """
3363        return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)

Create pending Sell limit-order (above current price). You must specify only 2 parameters: lots and target price to open sell limit-order. If you try to create sell limit-order below current price then broker immediately open Sell market order, such as if you do simple --sell operation! See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is open trade price for limit order.
Returns

JSON with response from broker server.

def SellStop( self, lots: int, targetPrice: float, limitPrice: float = 0.0, stopType: str = 'Limit', expDate: str = 'Undefined') -> dict:
3365    def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict:
3366        """
3367        Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order.
3368        In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP,
3369        `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to
3370        target price value then broker opens a limit order. See also: `Order()` docstring.
3371
3372        :param lots: volume, integer count of lots >= 1.
3373        :param targetPrice: target price > 0. This is trigger price for sell stop-order.
3374        :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order
3375                           with price equal to limitPrice, when current price goes to target price of sell stop-order.
3376        :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit"
3377                         for "Stop loss", "Take profit" and "Stop limit" types accordingly.
3378        :param expDate: string "Undefined" by default or local date in future.
3379                        String has a format like this: `%Y-%m-%d %H:%M:%S`.
3380                        This date is converting to UTC format for server.
3381        :return: JSON with response from broker server.
3382        """
3383        return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)

Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order. In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP, expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to target price value then broker opens a limit order. See also: Order() docstring.

Parameters
  • lots: volume, integer count of lots >= 1.
  • targetPrice: target price > 0. This is trigger price for sell stop-order.
  • limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
  • stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
  • expDate: string "Undefined" by default or local date in future. String has a format like this: %Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns

JSON with response from broker server.

def CloseOrders( self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3385    def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None:
3386        """
3387        Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`.
3388
3389        :param orderIDs: list of integers with `orderId` or `stopOrderId`.
3390        :param allOrdersIDs: pre-received lists of all active pending limit orders.
3391                             This avoids unnecessary downloading data from the server.
3392        :param allStopOrdersIDs: pre-received lists of all active stop orders.
3393        """
3394        if self.accountId is None or not self.accountId:
3395            uLogger.error("Variable `accountId` must be defined for using this method!")
3396            raise Exception("Account ID required")
3397
3398        if orderIDs:
3399            if allOrdersIDs is None:
3400                rawOrders = self.RequestPendingOrders()
3401                allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3402
3403            if allStopOrdersIDs is None:
3404                rawStopOrders = self.RequestStopOrders()
3405                allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3406
3407            for orderID in orderIDs:
3408                idInPendingOrders = orderID in allOrdersIDs
3409                idInStopOrders = orderID in allStopOrdersIDs
3410
3411                if not (idInPendingOrders or idInStopOrders):
3412                    uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID))
3413                    continue
3414
3415                else:
3416                    if idInPendingOrders:
3417                        uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID))
3418
3419                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder
3420                        self.body = str({"accountId": self.accountId, "orderId": orderID})
3421                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder"
3422                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3423
3424                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3425                            if self.moreDebug:
3426                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3427
3428                            uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID))
3429
3430                        else:
3431                            uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID))
3432
3433                    elif idInStopOrders:
3434                        uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID))
3435
3436                        # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder
3437                        self.body = str({"accountId": self.accountId, "stopOrderId": orderID})
3438                        closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder"
3439                        responseJSON = self.SendAPIRequest(closeURL, reqType="POST")
3440
3441                        if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]:
3442                            if self.moreDebug:
3443                                uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"]))
3444
3445                            uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID))
3446
3447                        else:
3448                            uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID))
3449
3450                    else:
3451                        continue

Cancel order or list of orders by its orderId or stopOrderId for current accountId.

Parameters
  • orderIDs: list of integers with orderId or stopOrderId.
  • allOrdersIDs: pre-received lists of all active pending limit orders. This avoids unnecessary downloading data from the server.
  • allStopOrdersIDs: pre-received lists of all active stop orders.
def CloseAllOrders(self) -> None:
3453    def CloseAllOrders(self) -> None:
3454        """
3455        Gets a list of open pending and stop orders and cancel it all.
3456        """
3457        rawOrders = self.RequestPendingOrders()
3458        allOrdersIDs = [item["orderId"] for item in rawOrders]  # all pending limit orders ID
3459        lenOrders = len(allOrdersIDs)
3460
3461        rawStopOrders = self.RequestStopOrders()
3462        allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders]  # all stop orders ID
3463        lenSOrders = len(allStopOrdersIDs)
3464
3465        if lenOrders > 0 or lenSOrders > 0:
3466            uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders))
3467
3468            self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs)
3469
3470        else:
3471            uLogger.info("Orders not found, nothing to cancel.")

Gets a list of open pending and stop orders and cancel it all.

def CloseAll(self, *args) -> None:
3473    def CloseAll(self, *args) -> None:
3474        """
3475        Close all available (not blocked) opened trades and orders.
3476
3477        Also, you can select one or more keywords case-insensitive:
3478        `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type.
3479
3480        Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods.
3481        """
3482        overview = self.Overview(show=False)  # get all open trades info
3483
3484        if len(args) == 0:
3485            uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...")
3486            self.CloseAllOrders()  # close all pending and stop orders
3487
3488            for iType in TKS_INSTRUMENTS:
3489                if iType != "Currencies":
3490                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies
3491
3492        else:
3493            uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args)))
3494            lowerArgs = [x.lower() for x in args]
3495
3496            if "orders" in lowerArgs:
3497                self.CloseAllOrders()  # close all pending and stop orders
3498
3499            for iType in TKS_INSTRUMENTS:
3500                if iType.lower() in lowerArgs and iType != "Currencies":
3501                    self.CloseAllTrades(iType, overview)  # close all positions of instruments with same type without currencies

Close all available (not blocked) opened trades and orders.

Also, you can select one or more keywords case-insensitive: orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.

Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.

def CloseAllByTicker(self, instrument: str) -> None:
3503    def CloseAllByTicker(self, instrument: str) -> None:
3504        """
3505        Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.
3506
3507        This method searches opened trade and orders of instrument throw all portfolio and then use
3508        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3509
3510        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3511
3512        :param instrument: string with ticker.
3513        """
3514        if instrument is None or not instrument:
3515            uLogger.error("Ticker name must be defined for using this method!")
3516            raise Exception("Ticker required")
3517
3518        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3519
3520        self._ticker = instrument  # try to set instrument as ticker
3521        self._figi = ""
3522
3523        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3524        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3525
3526        if limitAll and self.IsInLimitOrders(portfolio=overview):
3527            uLogger.debug("Closing all opened pending limit orders for the instrument with ticker [{}]. Wait, please...")
3528            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3529
3530        if stopAll and self.IsInStopOrders(portfolio=overview):
3531            uLogger.debug("Closing all opened stop orders for the instrument with ticker [{}]. Wait, please...")
3532            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3533
3534        if self.IsInPortfolio(portfolio=overview):
3535            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with ticker [{}]. Wait, please...")
3536            self.CloseTrades(instruments=[instrument], portfolio=overview)

Close all available (not blocked) opened trades and orders for one instrument defined by its ticker.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with ticker.
def CloseAllByFIGI(self, instrument: str) -> None:
3538    def CloseAllByFIGI(self, instrument: str) -> None:
3539        """
3540        Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.
3541
3542        This method searches opened trade and orders of instrument throw all portfolio and then use
3543        `CloseTrades()` and `CloseOrders()` methods to close trade and cancel all orders for that instrument.
3544
3545        See also: `IsInLimitOrders()`, `GetLimitOrderIDs()`, `IsInStopOrders()`, `GetStopOrderIDs()`, `CloseTrades()` and `CloseOrders()`.
3546
3547        :param instrument: string with FIGI id.
3548        """
3549        if instrument is None or not instrument:
3550            uLogger.error("FIGI id must be defined for using this method!")
3551            raise Exception("FIGI required")
3552
3553        overview = self.Overview(show=False)  # get user portfolio with all open trades info
3554
3555        self._ticker = ""
3556        self._figi = instrument  # try to set instrument as FIGI id
3557
3558        limitAll = [item["orderID"] for item in overview["stat"]["orders"]]  # list of all pending limit order IDs
3559        stopAll = [item["orderID"] for item in overview["stat"]["stopOrders"]]  # list of all stop order IDs
3560
3561        if limitAll and self.IsInLimitOrders(portfolio=overview):
3562            uLogger.debug("Closing all opened pending limit orders for the instrument with FIGI [{}]. Wait, please...")
3563            self.CloseOrders(orderIDs=self.GetLimitOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3564
3565        if stopAll and self.IsInStopOrders(portfolio=overview):
3566            uLogger.debug("Closing all opened stop orders for the instrument with FIGI [{}]. Wait, please...")
3567            self.CloseOrders(orderIDs=self.GetStopOrderIDs(portfolio=overview), allOrdersIDs=limitAll, allStopOrdersIDs=stopAll)
3568
3569        if self.IsInPortfolio(portfolio=overview):
3570            uLogger.debug("Closing all available (not blocked) opened trade for the instrument with FIGI [{}]. Wait, please...")
3571            self.CloseTrades(instruments=[instrument], portfolio=overview)

Close all available (not blocked) opened trades and orders for one instrument defined by its FIGI id.

This method searches opened trade and orders of instrument throw all portfolio and then use CloseTrades() and CloseOrders() methods to close trade and cancel all orders for that instrument.

See also: IsInLimitOrders(), GetLimitOrderIDs(), IsInStopOrders(), GetStopOrderIDs(), CloseTrades() and CloseOrders().

Parameters
  • instrument: string with FIGI id.
@staticmethod
def ParseOrderParameters(operation, **inputParameters):
3573    @staticmethod
3574    def ParseOrderParameters(operation, **inputParameters):
3575        """
3576        Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
3577
3578        :param operation: string "Buy" or "Sell".
3579        :param inputParameters: this is dict of strings that looks like this
3580               `{"lots": "L_int,...", "prices": "P_float,..."}` where
3581               "lots" key: one or more lot values (integer numbers) to open with every limit-order
3582               "prices" key: one or more prices to open limit-orders
3583               Counts of values in lots and prices lists must be equals!
3584        :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]`
3585        """
3586        # TODO: update order grid work with api v2
3587        pass
3588        # uLogger.debug("Input parameters: {}".format(inputParameters))
3589        #
3590        # if operation is None or not operation or operation not in ("Buy", "Sell"):
3591        #     uLogger.error("You must define operation type: 'Buy' or 'Sell'!")
3592        #     raise Exception("Incorrect value")
3593        #
3594        # if "l" in inputParameters.keys():
3595        #     inputParameters["lots"] = inputParameters.pop("l")
3596        #
3597        # if "p" in inputParameters.keys():
3598        #     inputParameters["prices"] = inputParameters.pop("p")
3599        #
3600        # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys():
3601        #     uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!")
3602        #     raise Exception("Incorrect value")
3603        #
3604        # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")]
3605        # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")]
3606        #
3607        # if len(lots) != len(prices):
3608        #     uLogger.error("'lots' and 'prices' lists must have equal length of values!")
3609        #     raise Exception("Incorrect value")
3610        #
3611        # uLogger.debug("Extracted parameters for orders:")
3612        # uLogger.debug("lots = {}".format(lots))
3613        # uLogger.debug("prices = {}".format(prices))
3614        #
3615        # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...]
3616        # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))]
3617        # uLogger.debug("Order parameters: {}".format(result))
3618        #
3619        # return result

Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.

Parameters
  • operation: string "Buy" or "Sell".
  • inputParameters: this is dict of strings that looks like this {"lots": "L_int,...", "prices": "P_float,..."} where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns

list of dictionaries with all lots and prices to open orders that looks like this [{"lot": lots_1, "price": price_1}, {...}, ...]

def IsInPortfolio(self, portfolio: dict = None) -> bool:
3621    def IsInPortfolio(self, portfolio: dict = None) -> bool:
3622        """
3623        Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`.
3624
3625        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3626        :return: `True` if portfolio contains open position with given instrument, `False` otherwise.
3627        """
3628        result = False
3629        msg = "Instrument not defined!"
3630
3631        if portfolio is None or not portfolio:
3632            portfolio = self.Overview(show=False)
3633
3634        if self._ticker:
3635            uLogger.debug("Searching instrument with ticker [{}] throw opened positions list...".format(self._ticker))
3636            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3637
3638            for iType in TKS_INSTRUMENTS:
3639                for instrument in portfolio["stat"][iType]:
3640                    if instrument["ticker"] == self._ticker:
3641                        result = True
3642                        msg = "Instrument with ticker [{}] is present in open positions".format(self._ticker)
3643                        break
3644
3645        elif self._figi:
3646            uLogger.debug("Searching instrument with FIGI [{}] throw opened positions list...".format(self._figi))
3647            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3648
3649            for iType in TKS_INSTRUMENTS:
3650                for instrument in portfolio["stat"][iType]:
3651                    if instrument["figi"] == self._figi:
3652                        result = True
3653                        msg = "Instrument with FIGI [{}] is present in open positions".format(self._figi)
3654                        break
3655
3656        else:
3657            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3658
3659        uLogger.debug(msg)
3660
3661        return result

Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if portfolio contains open position with given instrument, False otherwise.

def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3663    def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict:
3664        """
3665        Returns instrument from the user's portfolio if it presents there.
3666        Instrument must be defined by `ticker` (highly priority) or `figi`.
3667
3668        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3669        :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise.
3670        """
3671        result = None
3672        msg = "Instrument not defined!"
3673
3674        if portfolio is None or not portfolio:
3675            portfolio = self.Overview(show=False)
3676
3677        if self._ticker:
3678            uLogger.debug("Searching instrument with ticker [{}] in opened positions...".format(self._ticker))
3679            msg = "Instrument with ticker [{}] is not present in open positions".format(self._ticker)
3680
3681            for iType in TKS_INSTRUMENTS:
3682                for instrument in portfolio["stat"][iType]:
3683                    if instrument["ticker"] == self._ticker:
3684                        result = instrument
3685                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self._ticker, instrument["figi"])
3686                        break
3687
3688        elif self._figi:
3689            uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self._figi))
3690            msg = "Instrument with FIGI [{}] is not present in open positions".format(self._figi)
3691
3692            for iType in TKS_INSTRUMENTS:
3693                for instrument in portfolio["stat"][iType]:
3694                    if instrument["figi"] == self._figi:
3695                        result = instrument
3696                        msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self._figi)
3697                        break
3698
3699        else:
3700            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3701
3702        uLogger.debug(msg)
3703
3704        return result

Returns instrument from the user's portfolio if it presents there. Instrument must be defined by ticker (highly priority) or figi.

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

dict with instrument if portfolio contains open position with this instrument, None otherwise.

def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3706    def IsInLimitOrders(self, portfolio: dict = None) -> bool:
3707        """
3708        Checks if instrument is in the limit orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3709
3710        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3711
3712        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3713        :return: `True` if limit orders list contains some limit orders for the instrument, `False` otherwise.
3714        """
3715        result = False
3716        msg = "Instrument not defined!"
3717
3718        if portfolio is None or not portfolio:
3719            portfolio = self.Overview(show=False)
3720
3721        if self._ticker:
3722            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3723            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3724
3725            for instrument in portfolio["stat"]["orders"]:
3726                if instrument["ticker"] == self._ticker:
3727                    result = True
3728                    msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3729                    break
3730
3731        elif self._figi:
3732            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3733            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3734
3735            for instrument in portfolio["stat"]["orders"]:
3736                if instrument["figi"] == self._figi:
3737                    result = True
3738                    msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3739                    break
3740
3741        else:
3742            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3743
3744        uLogger.debug(msg)
3745
3746        return result

Checks if instrument is in the limit orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if limit orders list contains some limit orders for the instrument, False otherwise.

def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3748    def GetLimitOrderIDs(self, portfolio: dict = None) -> list[str]:
3749        """
3750        Returns list with all `orderID`s of opened pending limit orders for the instrument.
3751        Instrument must be defined by `ticker` (highly priority) or `figi`.
3752
3753        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3754
3755        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3756        :return: list with `orderID`s of limit orders.
3757        """
3758        result = []
3759        msg = "Instrument not defined!"
3760
3761        if portfolio is None or not portfolio:
3762            portfolio = self.Overview(show=False)
3763
3764        if self._ticker:
3765            uLogger.debug("Searching instrument with ticker [{}] throw opened pending limit orders list...".format(self._ticker))
3766            msg = "Instrument with ticker [{}] is not present in opened pending limit orders list".format(self._ticker)
3767
3768            for instrument in portfolio["stat"]["orders"]:
3769                if instrument["ticker"] == self._ticker:
3770                    result.append(instrument["orderID"])
3771
3772            if result:
3773                msg = "Instrument with ticker [{}] is present in limit orders list".format(self._ticker)
3774
3775        elif self._figi:
3776            uLogger.debug("Searching instrument with FIGI [{}] throw opened pending limit orders list...".format(self._figi))
3777            msg = "Instrument with FIGI [{}] is not present in opened pending limit orders list".format(self._figi)
3778
3779            for instrument in portfolio["stat"]["orders"]:
3780                if instrument["figi"] == self._figi:
3781                    result.append(instrument["orderID"])
3782
3783            if result:
3784                msg = "Instrument with FIGI [{}] is present in opened pending limit orders list".format(self._figi)
3785
3786        else:
3787            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3788
3789        uLogger.debug(msg)
3790
3791        return result

Returns list with all orderIDs of opened pending limit orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of limit orders.

def IsInStopOrders(self, portfolio: dict = None) -> bool:
3793    def IsInStopOrders(self, portfolio: dict = None) -> bool:
3794        """
3795        Checks if instrument is in the stop orders list. Instrument must be defined by `ticker` (highly priority) or `figi`.
3796
3797        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3798
3799        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3800        :return: `True` if stop orders list contains some stop orders for the instrument, `False` otherwise.
3801        """
3802        result = False
3803        msg = "Instrument not defined!"
3804
3805        if portfolio is None or not portfolio:
3806            portfolio = self.Overview(show=False)
3807
3808        if self._ticker:
3809            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3810            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3811
3812            for instrument in portfolio["stat"]["stopOrders"]:
3813                if instrument["ticker"] == self._ticker:
3814                    result = True
3815                    msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3816                    break
3817
3818        elif self._figi:
3819            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3820            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3821
3822            for instrument in portfolio["stat"]["stopOrders"]:
3823                if instrument["figi"] == self._figi:
3824                    result = True
3825                    msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3826                    break
3827
3828        else:
3829            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3830
3831        uLogger.debug(msg)
3832
3833        return result

Checks if instrument is in the stop orders list. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

True if stop orders list contains some stop orders for the instrument, False otherwise.

def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3835    def GetStopOrderIDs(self, portfolio: dict = None) -> list[str]:
3836        """
3837        Returns list with all `orderID`s of opened stop orders for the instrument.
3838        Instrument must be defined by `ticker` (highly priority) or `figi`.
3839
3840        See also: `CloseAllByTicker()` and `CloseAllByFIGI()`.
3841
3842        :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method.
3843        :return: list with `orderID`s of stop orders.
3844        """
3845        result = []
3846        msg = "Instrument not defined!"
3847
3848        if portfolio is None or not portfolio:
3849            portfolio = self.Overview(show=False)
3850
3851        if self._ticker:
3852            uLogger.debug("Searching instrument with ticker [{}] throw opened stop orders list...".format(self._ticker))
3853            msg = "Instrument with ticker [{}] is not present in opened stop orders list".format(self._ticker)
3854
3855            for instrument in portfolio["stat"]["stopOrders"]:
3856                if instrument["ticker"] == self._ticker:
3857                    result.append(instrument["orderID"])
3858
3859            if result:
3860                msg = "Instrument with ticker [{}] is present in stop orders list".format(self._ticker)
3861
3862        elif self._figi:
3863            uLogger.debug("Searching instrument with FIGI [{}] throw opened stop orders list...".format(self._figi))
3864            msg = "Instrument with FIGI [{}] is not present in opened stop orders list".format(self._figi)
3865
3866            for instrument in portfolio["stat"]["stopOrders"]:
3867                if instrument["figi"] == self._figi:
3868                    result.append(instrument["orderID"])
3869
3870            if result:
3871                msg = "Instrument with FIGI [{}] is present in opened stop orders list".format(self._figi)
3872
3873        else:
3874            uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!")
3875
3876        uLogger.debug(msg)
3877
3878        return result

Returns list with all orderIDs of opened stop orders for the instrument. Instrument must be defined by ticker (highly priority) or figi.

See also: CloseAllByTicker() and CloseAllByFIGI().

Parameters
  • portfolio: dict with user's portfolio data. If None, then requests portfolio from Overview() method.
Returns

list with orderIDs of stop orders.

def RequestLimits(self) -> dict:
3880    def RequestLimits(self) -> dict:
3881        """
3882        Method for obtaining the available funds for withdrawal for current `accountId`.
3883
3884        See also:
3885        - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
3886        - `OverviewLimits()` method
3887
3888        :return: dict with raw data from server that contains free funds for withdrawal. Example of dict:
3889                 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`.
3890                 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency
3891                 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures.
3892        """
3893        if self.accountId is None or not self.accountId:
3894            uLogger.error("Variable `accountId` must be defined for using this method!")
3895            raise Exception("Account ID required")
3896
3897        uLogger.debug("Requesting current available funds for withdrawal. Wait, please...")
3898
3899        self.body = str({"accountId": self.accountId})
3900        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits"
3901        rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
3902
3903        if self.moreDebug:
3904            uLogger.debug("Records about available funds for withdrawal successfully received")
3905
3906        return rawLimits

Method for obtaining the available funds for withdrawal for current accountId.

See also:

Returns

dict with raw data from server that contains free funds for withdrawal. Example of dict: {"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Here money is an array of portfolio currency positions, blocked is an array of blocked currency positions of the portfolio and blockedGuarantee is locked money under collateral for futures.

def OverviewLimits(self, show: bool = False) -> dict:
3908    def OverviewLimits(self, show: bool = False) -> dict:
3909        """
3910        Method for parsing and show table with available funds for withdrawal for current `accountId`.
3911
3912        See also: `RequestLimits()`.
3913
3914        :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log.
3915        :return: dict with raw parsed data from server and some calculated statistics about it.
3916        """
3917        if self.accountId is None or not self.accountId:
3918            uLogger.error("Variable `accountId` must be defined for using this method!")
3919            raise Exception("Account ID required")
3920
3921        rawLimits = self.RequestLimits()  # raw response with current available funds for withdrawal
3922
3923        view = {
3924            "rawLimits": rawLimits,
3925            "limits": {  # parsed data for every currency:
3926                "money": {  # this is an array of portfolio currency positions
3927                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"]
3928                },
3929                "blocked": {  # this is an array of blocked currency
3930                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"]
3931                },
3932                "blockedGuarantee": {  # this is locked money under collateral for futures
3933                    item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"]
3934                },
3935            },
3936        }
3937
3938        # --- Prepare text table with limits in human-readable format:
3939        if show:
3940            info = [
3941                "# Withdrawal limits\n\n",
3942                "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
3943                "* **Account ID:** [{}]\n".format(self.accountId),
3944            ]
3945
3946            if view["limits"]["money"]:
3947                info.extend([
3948                    "\n| Currencies | Total         | Available for withdrawal | Blocked for trade | Futures guarantee |\n",
3949                    "|------------|---------------|--------------------------|-------------------|-------------------|\n",
3950                ])
3951
3952            else:
3953                info.append("\nNo withdrawal limits\n")
3954
3955            for curr in view["limits"]["money"].keys():
3956                blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0
3957                blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0
3958                availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee)
3959
3960                infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format(
3961                    "[{}]".format(curr),
3962                    "{:.2f}".format(view["limits"]["money"][curr]),
3963                    "{:.2f}".format(availableMoney),
3964                    "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—",
3965                    "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—",
3966                )
3967
3968                if curr == "rub":
3969                    info.insert(5, infoStr)  # hack: insert "rub" at the first position in table and after headers
3970
3971                else:
3972                    info.append(infoStr)
3973
3974            infoText = "".join(info)
3975
3976            uLogger.info(infoText)
3977
3978            if self.withdrawalLimitsFile:
3979                with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH:
3980                    fH.write(infoText)
3981
3982                uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile)))
3983
3984                if self.useHTMLReports:
3985                    htmlFilePath = self.withdrawalLimitsFile.replace(".md", ".html") if self.withdrawalLimitsFile.endswith(".md") else self.withdrawalLimitsFile + ".html"
3986                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
3987                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Withdrawal limits", commonCSS=COMMON_CSS, markdown=infoText))
3988
3989                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
3990
3991        return view

Method for parsing and show table with available funds for withdrawal for current accountId.

See also: RequestLimits().

Parameters
  • show: if False then only dictionary returns, if True then also print withdrawal limits to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

def RequestAccounts(self) -> dict:
3993    def RequestAccounts(self) -> dict:
3994        """
3995        Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`.
3996
3997        See also:
3998        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
3999        - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
4000        - `OverviewUserInfo()` method
4001
4002        :return: dict with raw data from server that contains accounts info. Example of dict:
4003                 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account",
4004                   "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z",
4005                   "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`.
4006                 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now.
4007        """
4008        uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...")
4009
4010        self.body = str({})
4011        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts"
4012        rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST")
4013
4014        if self.moreDebug:
4015            uLogger.debug("Records about available accounts successfully received")
4016
4017        return rawAccounts

Method for requesting all brokerage accounts (accountIds) of current user detected by token.

See also:

Returns

dict with raw data from server that contains accounts info. Example of dict: {"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. If closedDate="1970-01-01T00:00:00Z" it means that account is active now.

def RequestUserInfo(self) -> dict:
4019    def RequestUserInfo(self) -> dict:
4020        """
4021        Method for requesting common user's information.
4022
4023        See also:
4024        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
4025        - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
4026        - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with
4027        - `OverviewUserInfo()` method
4028
4029        :return: dict with raw data from server that contains user's information. Example of dict:
4030                 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage",
4031                   "russian_shares", "structured_income_bonds"], "tariff": "premium"}`.
4032        """
4033        uLogger.debug("Requesting common user's information. Wait, please...")
4034
4035        self.body = str({})
4036        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo"
4037        rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST")
4038
4039        if self.moreDebug:
4040            uLogger.debug("Records about current user successfully received")
4041
4042        return rawUserInfo

Method for requesting common user's information.

See also:

Returns

dict with raw data from server that contains user's information. Example of dict: {"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.

def RequestMarginStatus(self, accountId: str = None) -> dict:
4044    def RequestMarginStatus(self, accountId: str = None) -> dict:
4045        """
4046        Method for requesting margin calculation for defined account ID.
4047
4048        See also:
4049        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
4050        - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
4051        - `OverviewUserInfo()` method
4052
4053        :param accountId: string with numeric account ID. If `None`, then used class field `accountId`.
4054        :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict.
4055                 Example of responses:
4056                 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`.
4057                 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000},
4058                                    "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000},
4059                                    "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000},
4060                                    "fundsSufficiencyLevel": {"units": "1", "nano": 280000000},
4061                                    "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`.
4062        """
4063        if accountId is None or not accountId:
4064            if self.accountId is None or not self.accountId:
4065                uLogger.error("Variable `accountId` must be defined for using this method!")
4066                raise Exception("Account ID required")
4067
4068            else:
4069                accountId = self.accountId  # use `self.accountId` (main ID) by default
4070
4071        uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId))
4072
4073        self.body = str({"accountId": accountId})
4074        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes"
4075        rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST")
4076
4077        if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}:
4078            uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId))
4079            rawMargin = {}
4080
4081        else:
4082            if self.moreDebug:
4083                uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId))
4084
4085        return rawMargin

Method for requesting margin calculation for defined account ID.

See also:

Parameters
  • accountId: string with numeric account ID. If None, then used class field accountId.
Returns

dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400: {"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns: {}. status code 200: {"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.

def RequestTariffLimits(self) -> dict:
4087    def RequestTariffLimits(self) -> dict:
4088        """
4089        Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`.
4090
4091        See also:
4092        - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
4093        - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
4094        - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
4095        - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
4096        - `OverviewUserInfo()` method
4097
4098        :return: dict with raw data from server that contains limits of current tariff. Example of dict:
4099                 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...],
4100                   "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`.
4101        """
4102        uLogger.debug("Requesting limits of current tariff. Wait, please...")
4103
4104        self.body = str({})
4105        portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff"
4106        rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST")
4107
4108        if self.moreDebug:
4109            uLogger.debug("Records with limits of current tariff successfully received")
4110
4111        return rawTariffLimits

Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.

See also:

Returns

dict with raw data from server that contains limits of current tariff. Example of dict: {"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.

def RequestBondCoupons(self, iJSON: dict) -> dict:
4113    def RequestBondCoupons(self, iJSON: dict) -> dict:
4114        """
4115        Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
4116        then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`.
4117        All dates are in UTC timezone.
4118
4119        REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons
4120        Documentation:
4121        - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
4122        - response: https://tinkoff.github.io/investAPI/instruments/#coupon
4123
4124        See also: `ExtendBondsData()`.
4125
4126        :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self._ticker]`
4127                      If raw iJSON is not data of bond then server returns an error [400] with message:
4128                      `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`.
4129        :return: dictionary with bond payment calendar. Response example
4130                 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12",
4131                   "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000},
4132                   "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z",
4133                   "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}`
4134        """
4135        if iJSON["figi"] is None or not iJSON["figi"]:
4136            uLogger.error("FIGI must be defined for using this method!")
4137            raise Exception("FIGI required")
4138
4139        startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z"
4140        endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z"
4141
4142        uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format(
4143            "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "",
4144            self._figi,
4145            startDate,
4146            endDate,
4147        ))
4148
4149        self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate})
4150        calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons"
4151        calendar = self.SendAPIRequest(calendarURL, reqType="POST")
4152
4153        if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}:
4154            uLogger.warning("Instrument type is not bond!")
4155
4156        else:
4157            if self.moreDebug:
4158                uLogger.debug("Records about bond payment calendar successfully received")
4159
4160        return calendar

Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z". All dates are in UTC timezone.

REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:

See also: ExtendBondsData().

Parameters
  • iJSON: raw json data of a bond from broker server, example iJSON = self.iList["Bonds"][self._ticker] If raw iJSON is not data of bond then server returns an error [400] with message: {"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns

dictionary with bond payment calendar. Response example {"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}

def ExtendBondsData( self, instruments: list[str], xlsx: bool = False) -> pandas.core.frame.DataFrame:
4162    def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame:
4163        """
4164        Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider
4165        Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar,
4166        coupon yields, current yields and some statistics etc.
4167
4168        WARNING! This is too long operation if a lot of bonds requested from broker server.
4169
4170        See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`.
4171
4172        :param instruments: list of strings with tickers or FIGIs.
4173        :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`,
4174                     for further used by data scientists or stock analytics.
4175        :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker.
4176                 In XLSX-file and Pandas DataFrame fields mean:
4177                 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond
4178                 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4179        """
4180        if instruments is None or not instruments:
4181            uLogger.error("List of tickers or FIGIs must be defined for using this method!")
4182            raise Exception("Ticker or FIGI required")
4183
4184        if isinstance(instruments, str):
4185            instruments = [instruments]
4186
4187        uniqueInstruments = self.GetUniqueFIGIs(instruments)
4188
4189        uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...")
4190
4191        iCount = len(uniqueInstruments)
4192        tooLong = iCount >= 20
4193        if tooLong:
4194            uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...")
4195
4196        bonds = None
4197        for i, self._figi in enumerate(uniqueInstruments):
4198            instrument = self.SearchByFIGI(requestPrice=False)  # raw data about instrument from server
4199
4200            if "type" in instrument.keys() and instrument["type"] == "Bonds":
4201                # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond
4202                rawBond = self.SearchByFIGI(requestPrice=True)
4203
4204                # Widen raw data with UTC current time (iData["actualDateTime"]):
4205                actualDate = datetime.now(tzutc())
4206                iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond
4207
4208                # Widen raw data with bond payment calendar (iData["rawCalendar"]):
4209                iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)}
4210
4211                # Replace some values with human-readable:
4212                iData["nominalCurrency"] = iData["nominal"]["currency"]
4213                iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"])
4214                iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"])
4215                iData["aciCurrency"] = iData["aciValue"]["currency"]
4216                iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"])
4217                iData["issueSize"] = int(iData["issueSize"])
4218                iData["issueSizePlan"] = int(iData["issueSizePlan"])
4219                iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]]
4220                iData["step"] = iData["step"] if "step" in iData.keys() else 0
4221                iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]]
4222                iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0
4223                iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0
4224                iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0
4225                iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0
4226                iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0
4227                iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0
4228
4229                # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date):
4230                iData["limitUpPercent"] = iData["currentPrice"]["limitUp"]  # max price on current day in percents of nominal
4231                iData["limitDownPercent"] = iData["currentPrice"]["limitDown"]  # min price on current day in percents of nominal
4232                iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"]  # last price on market in percents of nominal
4233                iData["closePricePercent"] = iData["currentPrice"]["closePrice"]  # previous day close in percents of nominal
4234                iData["changes"] = iData["currentPrice"]["changes"]  # this is percent of changes between `currentPrice` and `lastPrice`
4235                iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100  # max price on current day is `limitUpPercent` * `nominal`
4236                iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100  # min price on current day is `limitDownPercent` * `nominal`
4237                iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100  # last price on market is `lastPricePercent` * `nominal`
4238                iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100  # previous day close is `closePricePercent` * `nominal`
4239                iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"]  # this is delta between last deal price and last close
4240
4241                # Widen raw data with calendar data from `rawCalendar` values:
4242                calendarData = []
4243                if "events" in iData["rawCalendar"].keys():
4244                    for item in iData["rawCalendar"]["events"]:
4245                        calendarData.append({
4246                            "couponDate": item["couponDate"],
4247                            "couponNumber": int(item["couponNumber"]),
4248                            "fixDate": item["fixDate"] if "fixDate" in item.keys() else "",
4249                            "payCurrency": item["payOneBond"]["currency"],
4250                            "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]),
4251                            "couponType": TKS_COUPON_TYPES[item["couponType"]],
4252                            "couponStartDate": item["couponStartDate"],
4253                            "couponEndDate": item["couponEndDate"],
4254                            "couponPeriod": item["couponPeriod"],
4255                        })
4256
4257                    # if maturity date is unknown then uses the latest date in bond payment calendar for it:
4258                    if "maturityDate" not in iData.keys():
4259                        iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else ""
4260
4261                # Widen raw data with Coupon Rate.
4262                # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%:
4263                iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData])
4264                iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData])
4265                iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0.
4266
4267                # Widen raw data with Yield to Maturity (YTM) on current date.
4268                # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%:
4269                maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None
4270                iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None
4271                iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate])
4272                iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"]  # sum of all last coupons minus current ACI value
4273                iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0.
4274
4275                iData["calendar"] = calendarData  # adds calendar at the end
4276
4277                # Remove not used data:
4278                iData.pop("uid")
4279                iData.pop("positionUid")
4280                iData.pop("currentPrice")
4281                iData.pop("rawCalendar")
4282
4283                colNames = list(iData.keys())
4284                if bonds is None:
4285                    bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames))
4286
4287                else:
4288                    bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True)
4289
4290            else:
4291                uLogger.warning("Instrument is not a bond!")
4292
4293            processed = round(100 * (i + 1) / iCount, 1)
4294            if tooLong and processed % 5 == 0:
4295                uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount))
4296
4297            else:
4298                uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount))
4299
4300        bonds.index = bonds["ticker"].tolist()  # replace indexes with ticker names
4301
4302        # Saving bonds from Pandas DataFrame to XLSX sheet:
4303        if xlsx and self.bondsXLSXFile:
4304            with pd.ExcelWriter(
4305                    path=self.bondsXLSXFile,
4306                    date_format=TKS_DATE_FORMAT,
4307                    datetime_format=TKS_DATE_TIME_FORMAT,
4308                    mode="w",
4309            ) as writer:
4310                bonds.to_excel(
4311                    writer,
4312                    sheet_name="Extended bonds data",
4313                    index=True,
4314                    encoding="UTF-8",
4315                    freeze_panes=(1, 1),
4316                )  # saving as XLSX-file with freeze first row and column as headers
4317
4318            uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile)))
4319
4320        return bonds

Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().

Parameters
  • instruments: list of strings with tickers or FIGIs.
  • xlsx: if True then also exports Pandas DataFrame to xlsx-file bondsXLSXFile, default ext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns

wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon

def CreateBondsCalendar( self, extBonds: pandas.core.frame.DataFrame, xlsx: bool = False) -> pandas.core.frame.DataFrame:
4322    def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame:
4323        """
4324        Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default.
4325
4326        WARNING! This is too long operation if a lot of bonds requested from broker server.
4327
4328        See also: `ShowBondsCalendar()`, `ExtendBondsData()`.
4329
4330        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4331                        extended information about bonds: main info, current prices, bond payment calendar,
4332                        coupon yields, current yields and some statistics etc.
4333                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4334        :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default,
4335                     for further used by data scientists or stock analytics.
4336        :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4337        """
4338        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4339            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4340
4341        uLogger.debug("Generating bond payments calendar data. Wait, please...")
4342
4343        colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"]
4344        colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"]
4345        calendar = None
4346        for bond in extBonds.iterrows():
4347            for item in bond[1]["calendar"]:
4348                cData = {
4349                    "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()),
4350                    "couponDate": item["couponDate"],
4351                    "figi": bond[1]["figi"],
4352                    "ticker": bond[1]["ticker"],
4353                    "name": bond[1]["name"],
4354                    "couponNumber": item["couponNumber"],
4355                    "payOneBond": item["payOneBond"],
4356                    "payCurrency": item["payCurrency"],
4357                    "couponType": item["couponType"],
4358                    "couponPeriod": item["couponPeriod"],
4359                    "fixDate": item["fixDate"],
4360                    "couponStartDate": item["couponStartDate"],
4361                    "couponEndDate": item["couponEndDate"],
4362                }
4363
4364                if calendar is None:
4365                    calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID))
4366
4367                else:
4368                    calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True)
4369
4370        if calendar is not None:
4371            calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True)  # sort all payments for all bonds by payment date
4372
4373            # Saving calendar from Pandas DataFrame to XLSX sheet:
4374            if xlsx:
4375                xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx"
4376
4377                with pd.ExcelWriter(
4378                        path=xlsxCalendarFile,
4379                        date_format=TKS_DATE_FORMAT,
4380                        datetime_format=TKS_DATE_TIME_FORMAT,
4381                        mode="w",
4382                ) as writer:
4383                    humanReadable = calendar.copy(deep=True)
4384                    humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0])
4385                    humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0])
4386                    humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0])
4387                    humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0])
4388                    humanReadable.columns = colNames  # human-readable column names
4389
4390                    humanReadable.to_excel(
4391                        writer,
4392                        sheet_name="Bond payments calendar",
4393                        index=False,
4394                        encoding="UTF-8",
4395                        freeze_panes=(1, 2),
4396                    )  # saving as XLSX-file with freeze first row and column as headers
4397
4398                    del humanReadable  # release df in memory
4399
4400                uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile)))
4401
4402        return calendar

Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.

WARNING! This is too long operation if a lot of bonds requested from broker server.

See also: ShowBondsCalendar(), ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • xlsx: if True then also exports Pandas DataFrame to file calendarFile + ".xlsx", calendar.xlsx by default, for further used by data scientists or stock analytics.
Returns

Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon

def ShowBondsCalendar(self, extBonds: pandas.core.frame.DataFrame, show: bool = True) -> str:
4404    def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str:
4405        """
4406        Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond.
4407        Also, creates Markdown file with calendar data, `calendar.md` by default.
4408
4409        See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`.
4410
4411        :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains
4412                        extended information about bonds: main info, current prices, bond payment calendar,
4413                        coupon yields, current yields and some statistics etc.
4414                        If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`.
4415        :param show: if `True` then also printing bonds payment calendar to the console,
4416                     otherwise save to file `calendarFile` only. `False` by default.
4417        :return: multilines text in Markdown format with bonds payment calendar as a table.
4418        """
4419        if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty:
4420            extBonds = self.ExtendBondsData(instruments=[self._figi, self._ticker], xlsx=False)
4421
4422        infoText = "# Bond payments calendar\n\n"
4423
4424        calendar = self.CreateBondsCalendar(extBonds, xlsx=True)  # generate Pandas DataFrame with full calendar data
4425
4426        if not (calendar is None or calendar.empty):
4427            splitLine = "|       |                 |              |              |     |               |           |        |                   |\n"
4428
4429            info = [
4430                "* **Actual on date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4431                "| Paid  | Payment date    | FIGI         | Ticker       | No. | Value         | Type      | Period | End registry date |\n",
4432                "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n",
4433            ]
4434
4435            newMonth = False
4436            notOneBond = calendar["figi"].nunique() > 1
4437            for i, bond in enumerate(calendar.iterrows()):
4438                if newMonth and notOneBond:
4439                    info.append(splitLine)
4440
4441                info.append(
4442                    "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format(
4443                        "  √" if bond[1]["paid"] else "  —",
4444                        bond[1]["couponDate"].split("T")[0],
4445                        bond[1]["figi"],
4446                        bond[1]["ticker"],
4447                        bond[1]["couponNumber"],
4448                        "{} {}".format(
4449                            "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."),
4450                            bond[1]["payCurrency"],
4451                        ),
4452                        bond[1]["couponType"],
4453                        bond[1]["couponPeriod"],
4454                        bond[1]["fixDate"].split("T")[0],
4455                    )
4456                )
4457
4458                if i < len(calendar.values) - 1:
4459                    curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4460                    nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc())
4461                    newMonth = False if curDate.month == nextDate.month else True
4462
4463                else:
4464                    newMonth = False
4465
4466            infoText += "".join(info)
4467
4468            if show:
4469                uLogger.info("{}".format(infoText))
4470
4471            if self.calendarFile is not None:
4472                with open(self.calendarFile, "w", encoding="UTF-8") as fH:
4473                    fH.write(infoText)
4474
4475                uLogger.info("Bond payments calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile)))
4476
4477                if self.useHTMLReports:
4478                    htmlFilePath = self.calendarFile.replace(".md", ".html") if self.calendarFile.endswith(".md") else self.calendarFile + ".html"
4479                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4480                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="Bond payments calendar", commonCSS=COMMON_CSS, markdown=infoText))
4481
4482                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4483
4484        else:
4485            infoText += "No data\n"
4486
4487        return infoText

Show bond payments calendar as a table. One row in input bonds dataframe contains one bond. Also, creates Markdown file with calendar data, calendar.md by default.

See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().

Parameters
  • extBonds: Pandas DataFrame object returns by ExtendBondsData() method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter is None then used figi or ticker as bond name and then calculate ExtendBondsData().
  • show: if True then also printing bonds payment calendar to the console, otherwise save to file calendarFile only. False by default.
Returns

multilines text in Markdown format with bonds payment calendar as a table.

def OverviewAccounts(self, show: bool = False) -> dict:
4489    def OverviewAccounts(self, show: bool = False) -> dict:
4490        """
4491        Method for parsing and show simple table with all available user accounts.
4492
4493        See also: `RequestAccounts()` and `OverviewUserInfo()` methods.
4494
4495        :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log.
4496        :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict:
4497                 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...},
4498                          "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1",
4499                                                        "status": "Opened and active account", "opened": "2018-05-23 00:00:00",
4500                                                        "closed": "—", "access": "Full access" }, ...}}`
4501        """
4502        rawAccounts = self.RequestAccounts()  # Raw responses with accounts
4503
4504        # This is an array of dict with user accounts, its `accountId`s and some parsed data:
4505        accounts = {
4506            item["id"]: {
4507                "type": TKS_ACCOUNT_TYPES[item["type"]],
4508                "name": item["name"],
4509                "status": TKS_ACCOUNT_STATUSES[item["status"]],
4510                "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4511                "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—",
4512                "access": TKS_ACCESS_LEVELS[item["accessLevel"]],
4513            } for item in rawAccounts["accounts"]
4514        }
4515
4516        # Raw and parsed data with some fields replaced in "stat" section:
4517        view = {
4518            "rawAccounts": rawAccounts,
4519            "stat": accounts,
4520        }
4521
4522        # --- Prepare simple text table with only accounts data in human-readable format:
4523        if show:
4524            info = [
4525                "# User accounts\n\n",
4526                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4527                "| Account ID   | Type                      | Status                    | Name                           |\n",
4528                "|--------------|---------------------------|---------------------------|--------------------------------|\n",
4529            ]
4530
4531            for account in view["stat"].keys():
4532                info.extend([
4533                    "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format(
4534                        account,
4535                        view["stat"][account]["type"],
4536                        view["stat"][account]["status"],
4537                        view["stat"][account]["name"],
4538                    )
4539                ])
4540
4541            infoText = "".join(info)
4542
4543            uLogger.info(infoText)
4544
4545            if self.userAccountsFile:
4546                with open(self.userAccountsFile, "w", encoding="UTF-8") as fH:
4547                    fH.write(infoText)
4548
4549                uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile)))
4550
4551                if self.useHTMLReports:
4552                    htmlFilePath = self.userAccountsFile.replace(".md", ".html") if self.userAccountsFile.endswith(".md") else self.userAccountsFile + ".html"
4553                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4554                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User accounts", commonCSS=COMMON_CSS, markdown=infoText))
4555
4556                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4557
4558        return view

Method for parsing and show simple table with all available user accounts.

See also: RequestAccounts() and OverviewUserInfo() methods.

Parameters
  • show: if False then only dictionary with accounts data returns, if True then also print it to log.
Returns

dict with parsed accounts data received from RequestAccounts() method. Example of dict: view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}

def OverviewUserInfo(self, show: bool = False) -> dict:
4560    def OverviewUserInfo(self, show: bool = False) -> dict:
4561        """
4562        Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit).
4563
4564        See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods.
4565
4566        :param show: if `False` then only dictionary returns, if `True` then also print user's data to log.
4567        :return: dict with raw parsed data from server and some calculated statistics about it.
4568        """
4569        overview = self.Overview(show=False)  # Request current user portfolio for the ability to calculate missing funds
4570        tmpTicker = self._ticker
4571        self._ticker = "RUB000UTSTOM"  # This instrument show in rub how much money cost current margin
4572        missing = self.GetInstrumentFromPortfolio(portfolio=overview)
4573        self._ticker = tmpTicker
4574
4575        rawUserInfo = self.RequestUserInfo()  # Raw response with common user info
4576        overviewAccount = self.OverviewAccounts(show=False)  # Raw and parsed accounts data
4577        rawAccounts = overviewAccount["rawAccounts"]  # Raw response with user accounts data
4578        accounts = overviewAccount["stat"]  # Dict with only statistics about user accounts
4579        rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()}  # Raw response with margin calculation for every account ID
4580        rawTariffLimits = self.RequestTariffLimits()  # Raw response with limits of current tariff
4581
4582        # This is dict with parsed common user data:
4583        userInfo = {
4584            "premium": "Yes" if rawUserInfo["premStatus"] else "No",
4585            "qualified": "Yes" if rawUserInfo["qualStatus"] else "No",
4586            "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]],
4587            "tariff": rawUserInfo["tariff"],
4588        }
4589
4590        # This is an array of dict with parsed margin statuses for every account IDs:
4591        margins = {}
4592        for accountId in accounts.keys():
4593            if rawMargins[accountId]:
4594                margins[accountId] = {
4595                    "currency": rawMargins[accountId]["liquidPortfolio"]["currency"],
4596                    "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]),
4597                    "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]),
4598                    "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]),
4599                    "diff": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]),
4600                    "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]),
4601                    "missing": missing["volume"],
4602                }
4603
4604            else:
4605                margins[accountId] = {}  # Server response: margin status is disabled for current accountId
4606
4607        unary = {}  # unary-connection limits
4608        for item in rawTariffLimits["unaryLimits"]:
4609            if item["limitPerMinute"] in unary.keys():
4610                unary[item["limitPerMinute"]].extend(item["methods"])
4611
4612            else:
4613                unary[item["limitPerMinute"]] = item["methods"]
4614
4615        stream = {}  # stream-connection limits
4616        for item in rawTariffLimits["streamLimits"]:
4617            if item["limit"] in stream.keys():
4618                stream[item["limit"]].extend(item["streams"])
4619
4620            else:
4621                stream[item["limit"]] = item["streams"]
4622
4623        # This is dict with parsed limits of current tariff (connections, API methods etc.):
4624        limits = {
4625            "unary": unary,
4626            "stream": stream,
4627        }
4628
4629        # Raw and parsed data as an output result:
4630        view = {
4631            "rawUserInfo": rawUserInfo,
4632            "rawAccounts": rawAccounts,
4633            "rawMargins": rawMargins,
4634            "rawTariffLimits": rawTariffLimits,
4635            "stat": {
4636                "overview": overview,
4637                "userInfo": userInfo,
4638                "accounts": accounts,
4639                "margins": margins,
4640                "limits": limits,
4641            },
4642        }
4643
4644        # --- Prepare text table with user information in human-readable format:
4645        if show:
4646            info = [
4647                "# Full user information\n\n",
4648                "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)),
4649                "## Common information\n\n",
4650                "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]),
4651                "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]),
4652                "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]),
4653                "* **Allowed to work with instruments:**\n{}\n".format("".join(["  - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])),
4654                "\n## User accounts\n\n",
4655            ]
4656
4657            for account in view["stat"]["accounts"].keys():
4658                info.extend([
4659                    "### ID: [{}]\n\n".format(account),
4660                    "| Parameters           | Values                                                       |\n",
4661                    "|----------------------|--------------------------------------------------------------|\n",
4662                    "| Account type:        | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]),
4663                    "| Account name:        | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]),
4664                    "| Account status:      | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]),
4665                    "| Access level:        | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]),
4666                    "| Date opened:         | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]),
4667                    "| Date closed:         | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]),
4668                ])
4669
4670                if margins[account]:
4671                    info.extend([
4672                        "| Margin status:       | Enabled                                                      |\n",
4673                        "| - Liquid portfolio:  | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])),
4674                        "| - Margin starting:   | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])),
4675                        "| - Margin minimum:    | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])),
4676                        "| - Margin difference: | {:<60} |\n".format("{} {}".format(margins[account]["diff"], margins[account]["currency"])),
4677                        "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)),
4678                        "| - Not covered funds: | {:<60} |\n\n".format("{:.2f} {}".format(margins[account]["missing"], margins[account]["currency"])),
4679                    ])
4680
4681                else:
4682                    info.append("| Margin status:       | Disabled                                                     |\n\n")
4683
4684            info.extend([
4685                "\n## Current user tariff limits\n",
4686                "\n### See also\n",
4687                "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n",
4688                "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n",
4689                "  - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n",
4690                "  - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n",
4691                "\n### Unary limits\n",
4692            ])
4693
4694            if unary:
4695                for key, values in sorted(unary.items()):
4696                    info.append("\n* Max requests per minute: {}\n".format(key))
4697
4698                    for value in values:
4699                        info.append("  - {}\n".format(value))
4700
4701            else:
4702                info.append("\nNot available\n")
4703
4704            info.append("\n### Stream limits\n")
4705
4706            if stream:
4707                for key, values in sorted(stream.items()):
4708                    info.append("\n* Max stream connections: {}\n".format(key))
4709
4710                    for value in values:
4711                        info.append("  - {}\n".format(value))
4712
4713            else:
4714                info.append("\nNot available\n")
4715
4716            infoText = "".join(info)
4717
4718            uLogger.info(infoText)
4719
4720            if self.userInfoFile:
4721                with open(self.userInfoFile, "w", encoding="UTF-8") as fH:
4722                    fH.write(infoText)
4723
4724                uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile)))
4725
4726                if self.useHTMLReports:
4727                    htmlFilePath = self.userInfoFile.replace(".md", ".html") if self.userInfoFile.endswith(".md") else self.userInfoFile + ".html"
4728                    with open(htmlFilePath, "w", encoding="UTF-8") as fH:
4729                        fH.write(Template(text=MAIN_INFO_TEMPLATE).render(mainTitle="User info", commonCSS=COMMON_CSS, markdown=infoText))
4730
4731                    uLogger.info("The report has also been converted to an HTML file: [{}]".format(os.path.abspath(htmlFilePath)))
4732
4733        return view

Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).

See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.

Parameters
  • show: if False then only dictionary returns, if True then also print user's data to log.
Returns

dict with raw parsed data from server and some calculated statistics about it.

class Args:
4736class Args:
4737    """
4738    If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object.
4739    """
4740    def __init__(self, **kwargs):
4741        self.__dict__.update(kwargs)
4742
4743    def __getattr__(self, item):
4744        return None

If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.

Args(**kwargs)
4740    def __init__(self, **kwargs):
4741        self.__dict__.update(kwargs)
def ParseArgs():
4747def ParseArgs():
4748    """This function get and parse command line keys."""
4749    parser = ArgumentParser()  # command-line string parser
4750
4751    parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md"
4752    parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]"
4753
4754    # --- options:
4755
4756    parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.")
4757    parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/")
4758    parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.")
4759
4760    parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.")
4761    parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).")
4762
4763    parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.")
4764    parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.")
4765
4766    parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.")
4767    parser.add_argument("--html", "--HTML", action="store_true", default=False, help="Option: if key present then TKSBrokerAPI generate also HTML reports from Markdown. False by default.")
4768
4769    parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.")
4770    parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.")
4771    parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.")
4772
4773    parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.")
4774    parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.")
4775
4776    # --- commands:
4777
4778    parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.")
4779
4780    parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.")
4781    parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.")
4782    parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4783    parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.")
4784    parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!")
4785    parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.")
4786    parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!")
4787    parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.")
4788
4789    parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.")
4790    parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.")
4791    parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.")
4792    parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.")
4793    parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.")
4794    parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.")
4795
4796    parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.")
4797    parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.")
4798    parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.")
4799    parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).")
4800
4801    parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.")
4802    parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4803    parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].")
4804
4805    parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.")
4806    parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!")
4807    parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!")
4808    parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4809    parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.")
4810    # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4811    # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!")
4812
4813    parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4814    parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.")
4815    parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.")
4816    parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.")
4817    parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations. If the `--close-all` key present with the `--ticker` or `--figi` keys, then positions and all open limit and stop orders for the specified instrument are closed.")
4818
4819    parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.")
4820    parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.")
4821    parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.")
4822
4823    cmdArgs = parser.parse_args()
4824    return cmdArgs

This function get and parse command line keys.

def Main(**kwargs):
4827def Main(**kwargs):
4828    """
4829    Main function for work with TKSBrokerAPI in the console.
4830
4831    See examples:
4832    - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md
4833    - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md
4834    """
4835    args = Args(**kwargs) if kwargs else ParseArgs()  # get and parse command-line parameters or use **kwarg parameters
4836
4837    if args.debug_level:
4838        uLogger.level = 10  # always debug level by default
4839        uLogger.handlers[0].level = args.debug_level  # level for STDOUT
4840
4841    exitCode = 0
4842    start = datetime.now(tzutc())
4843    uLogger.debug("=-" * 50)
4844    uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format(
4845        start.strftime(TKS_PRINT_DATE_TIME_FORMAT),
4846        start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
4847    ))
4848
4849    # trying to calculate full current version:
4850    buildVersion = __version__
4851    try:
4852        v = version("tksbrokerapi")
4853        buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0"  # set version as major.minor.dev0 if run as local build or local script
4854
4855    except Exception:
4856        buildVersion = __version__ + ".dev0"  # if an errors occurred then also set version as major.minor.dev0
4857
4858    uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion))
4859    uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT))
4860
4861    try:
4862        if args.version:
4863            print("TKSBrokerAPI {}".format(buildVersion))
4864            uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion))
4865
4866        else:
4867            # Init class for trading with Tinkoff Broker:
4868            trader = TinkoffBrokerServer(
4869                token=args.token,
4870                accountId=args.account_id,
4871                useCache=not args.no_cache,
4872            )
4873
4874            # --- set some options:
4875
4876            if args.more:
4877                trader.moreDebug = True
4878                uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.")
4879
4880            if args.html:
4881                trader.useHTMLReports = True
4882
4883            if args.ticker:
4884                ticker = str(args.ticker).upper()  # Tickers may be upper case only
4885
4886                if ticker in trader.aliasesKeys:
4887                    trader.ticker = trader.aliases[ticker]  # Replace some tickers with its aliases
4888
4889                else:
4890                    trader.ticker = ticker
4891
4892            if args.figi:
4893                trader.figi = str(args.figi).upper()  # FIGIs may be upper case only
4894
4895            if args.depth is not None:
4896                trader.depth = args.depth
4897
4898            # --- do one command:
4899
4900            if args.list:
4901                if args.output is not None:
4902                    trader.instrumentsFile = args.output
4903
4904                trader.ShowInstrumentsInfo(show=True)
4905
4906            elif args.list_xlsx:
4907                trader.DumpInstrumentsAsXLSX(forceUpdate=False)
4908
4909            elif args.bonds_xlsx is not None:
4910                if args.output is not None:
4911                    trader.bondsXLSXFile = args.output
4912
4913                if len(args.bonds_xlsx) == 0:
4914                    trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True)  # request bonds with all available tickers
4915
4916                else:
4917                    trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True)  # request list of given bonds
4918
4919            elif args.search:
4920                if args.output is not None:
4921                    trader.searchResultsFile = args.output
4922
4923                trader.SearchInstruments(pattern=args.search[0], show=True)
4924
4925            elif args.info:
4926                if not (args.ticker or args.figi):
4927                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4928                    raise Exception("Ticker or FIGI required")
4929
4930                if args.output is not None:
4931                    trader.infoFile = args.output
4932
4933                if args.ticker:
4934                    trader.SearchByTicker(requestPrice=True, show=True)  # show info and current prices by ticker name
4935
4936                else:
4937                    trader.SearchByFIGI(requestPrice=True, show=True)  # show info and current prices by FIGI id
4938
4939            elif args.calendar is not None:
4940                if args.output is not None:
4941                    trader.calendarFile = args.output
4942
4943                if len(args.calendar) == 0:
4944                    bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False)  # request bonds with all available tickers
4945
4946                else:
4947                    bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False)  # request list of given bonds
4948
4949                trader.ShowBondsCalendar(extBonds=bondsData, show=True)  # shows bonds payment calendar only
4950
4951            elif args.price:
4952                if not (args.ticker or args.figi):
4953                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
4954                    raise Exception("Ticker or FIGI required")
4955
4956                trader.GetCurrentPrices(show=True)
4957
4958            elif args.prices is not None:
4959                if args.output is not None:
4960                    trader.pricesFile = args.output
4961
4962                trader.GetListOfPrices(instruments=args.prices, show=True)  # WARNING: too long wait for a lot of instruments prices
4963
4964            elif args.overview:
4965                if args.output is not None:
4966                    trader.overviewFile = args.output
4967
4968                trader.Overview(show=True, details="full")
4969
4970            elif args.overview_digest:
4971                if args.output is not None:
4972                    trader.overviewDigestFile = args.output
4973
4974                trader.Overview(show=True, details="digest")
4975
4976            elif args.overview_positions:
4977                if args.output is not None:
4978                    trader.overviewPositionsFile = args.output
4979
4980                trader.Overview(show=True, details="positions")
4981
4982            elif args.overview_orders:
4983                if args.output is not None:
4984                    trader.overviewOrdersFile = args.output
4985
4986                trader.Overview(show=True, details="orders")
4987
4988            elif args.overview_analytics:
4989                if args.output is not None:
4990                    trader.overviewAnalyticsFile = args.output
4991
4992                trader.Overview(show=True, details="analytics")
4993
4994            elif args.overview_calendar:
4995                if args.output is not None:
4996                    trader.overviewAnalyticsFile = args.output
4997
4998                trader.Overview(show=True, details="calendar")
4999
5000            elif args.deals is not None:
5001                if args.output is not None:
5002                    trader.reportFile = args.output
5003
5004                if 0 <= len(args.deals) < 3:
5005                    trader.Deals(
5006                        start=args.deals[0] if len(args.deals) >= 1 else None,
5007                        end=args.deals[1] if len(args.deals) == 2 else None,
5008                        show=True,  # Always show deals report in console
5009                        showCancelled=not args.no_cancelled,  # If --no-cancelled key then remove cancelled operations from the deals report. False by default.
5010                    )
5011
5012                else:
5013                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5014                    raise Exception("Incorrect value")
5015
5016            elif args.history is not None:
5017                if args.output is not None:
5018                    trader.historyFile = args.output
5019
5020                if 0 <= len(args.history) < 3:
5021                    dataReceived = trader.History(
5022                        start=args.history[0] if len(args.history) >= 1 else None,
5023                        end=args.history[1] if len(args.history) == 2 else None,
5024                        interval="hour" if args.interval is None or not args.interval else args.interval,
5025                        onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing,
5026                        csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep,
5027                        show=True,  # shows all downloaded candles in console
5028                    )
5029
5030                    if args.render_chart is not None and dataReceived is not None:
5031                        iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5032
5033                        trader.ShowHistoryChart(
5034                            candles=dataReceived,
5035                            interact=iChart,
5036                            openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5037                        )
5038
5039                else:
5040                    uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]")
5041                    raise Exception("Incorrect value")
5042
5043            elif args.load_history is not None:
5044                histData = trader.LoadHistory(filePath=args.load_history)  # load data from file and show history in console
5045
5046                if args.render_chart is not None and histData is not None:
5047                    iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True
5048                    trader.ticker = os.path.basename(args.load_history)  # use filename as ticker name for PriceGenerator's chart
5049
5050                    trader.ShowHistoryChart(
5051                        candles=histData,
5052                        interact=iChart,
5053                        openInBrowser=False,  # False by default, to avoid issues with `permissions denied` to html-file.
5054                    )
5055
5056            elif args.trade is not None:
5057                if 1 <= len(args.trade) <= 5:
5058                    trader.Trade(
5059                        operation=args.trade[0],
5060                        lots=int(args.trade[1]) if len(args.trade) >= 2 else 1,
5061                        tp=float(args.trade[2]) if len(args.trade) >= 3 else 0.,
5062                        sl=float(args.trade[3]) if len(args.trade) >= 4 else 0.,
5063                        expDate=args.trade[4] if len(args.trade) == 5 else "Undefined",
5064                    )
5065
5066                else:
5067                    uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5068
5069            elif args.buy is not None:
5070                if 0 <= len(args.buy) <= 4:
5071                    trader.Buy(
5072                        lots=int(args.buy[0]) if len(args.buy) >= 1 else 1,
5073                        tp=float(args.buy[1]) if len(args.buy) >= 2 else 0.,
5074                        sl=float(args.buy[2]) if len(args.buy) >= 3 else 0.,
5075                        expDate=args.buy[3] if len(args.buy) == 4 else "Undefined",
5076                    )
5077
5078                else:
5079                    uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5080
5081            elif args.sell is not None:
5082                if 0 <= len(args.sell) <= 4:
5083                    trader.Sell(
5084                        lots=int(args.sell[0]) if len(args.sell) >= 1 else 1,
5085                        tp=float(args.sell[1]) if len(args.sell) >= 2 else 0.,
5086                        sl=float(args.sell[2]) if len(args.sell) >= 3 else 0.,
5087                        expDate=args.sell[3] if len(args.sell) == 4 else "Undefined",
5088                    )
5089
5090                else:
5091                    uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5092
5093            elif args.order:
5094                if 4 <= len(args.order) <= 7:
5095                    trader.Order(
5096                        operation=args.order[0],
5097                        orderType=args.order[1],
5098                        lots=int(args.order[2]),
5099                        targetPrice=float(args.order[3]),
5100                        limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0.,
5101                        stopType=args.order[5] if len(args.order) >= 6 else "Limit",
5102                        expDate=args.order[6] if len(args.order) == 7 else "Undefined",
5103                    )
5104
5105                else:
5106                    uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`")
5107
5108            elif args.buy_limit:
5109                trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1])
5110
5111            elif args.sell_limit:
5112                trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1])
5113
5114            elif args.buy_stop:
5115                if 2 <= len(args.buy_stop) <= 7:
5116                    trader.BuyStop(
5117                        lots=int(args.buy_stop[0]),
5118                        targetPrice=float(args.buy_stop[1]),
5119                        limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0.,
5120                        stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit",
5121                        expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined",
5122                    )
5123
5124                else:
5125                    uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`")
5126
5127            elif args.sell_stop:
5128                if 2 <= len(args.sell_stop) <= 7:
5129                    trader.SellStop(
5130                        lots=int(args.sell_stop[0]),
5131                        targetPrice=float(args.sell_stop[1]),
5132                        limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0.,
5133                        stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit",
5134                        expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined",
5135                    )
5136
5137                else:
5138                    uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help")
5139
5140            # elif args.buy_order_grid is not None:
5141            #     # update order grid work with api v2
5142            #     if len(args.buy_order_grid) == 2:
5143            #         orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid))
5144            #
5145            #         for order in orderParams:
5146            #             trader.Order(operation="Buy", lots=order["lot"], price=order["price"])
5147            #
5148            #     else:
5149            #         uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5150            #
5151            # elif args.sell_order_grid is not None:
5152            #     # update order grid work with api v2
5153            #     if len(args.sell_order_grid) >= 2:
5154            #         orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid))
5155            #
5156            #         for order in orderParams:
5157            #             trader.Order(operation="Sell", lots=order["lot"], price=order["price"])
5158            #
5159            #     else:
5160            #         uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`")
5161
5162            elif args.close_order is not None:
5163                trader.CloseOrders(args.close_order)  # close only one order
5164
5165            elif args.close_orders is not None:
5166                trader.CloseOrders(args.close_orders)  # close list of orders
5167
5168            elif args.close_trade:
5169                if not (args.ticker or args.figi):
5170                    uLogger.error("`--ticker` key or `--figi` key is required for this operation!")
5171                    raise Exception("Ticker or FIGI required")
5172
5173                if args.ticker:
5174                    trader.CloseTrades([str(args.ticker).upper()])  # close only one trade by ticker (priority)
5175
5176                else:
5177                    trader.CloseTrades([str(args.figi).upper()])  # close only one trade by FIGI
5178
5179            elif args.close_trades is not None:
5180                trader.CloseTrades(args.close_trades)  # close trades for list of tickers
5181
5182            elif args.close_all is not None:
5183                if args.ticker:
5184                    trader.CloseAllByTicker(instrument=str(args.ticker).upper())
5185
5186                elif args.figi:
5187                    trader.CloseAllByFIGI(instrument=str(args.figi).upper())
5188
5189                else:
5190                    trader.CloseAll(*args.close_all)
5191
5192            elif args.limits:
5193                if args.output is not None:
5194                    trader.withdrawalLimitsFile = args.output
5195
5196                trader.OverviewLimits(show=True)
5197
5198            elif args.user_info:
5199                if args.output is not None:
5200                    trader.userInfoFile = args.output
5201
5202                trader.OverviewUserInfo(show=True)
5203
5204            elif args.account:
5205                if args.output is not None:
5206                    trader.userAccountsFile = args.output
5207
5208                trader.OverviewAccounts(show=True)
5209
5210            else:
5211                uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.")
5212                raise Exception("There is no command to execute")
5213
5214    except Exception:
5215        trace = tb.format_exc()
5216        for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]:
5217            if e in trace:
5218                uLogger.error("Check your Internet connection! Failed to establish connection to broker server!")
5219                break
5220
5221        uLogger.debug(trace)
5222        uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues")
5223        exitCode = 255  # an error occurred, must be open a ticket for this issue
5224
5225    finally:
5226        finish = datetime.now(tzutc())
5227
5228        if exitCode == 0:
5229            if args.more:
5230                uLogger.debug("All operations were finished success (summary code is 0).")
5231
5232        else:
5233            uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format(
5234                os.path.abspath(uLog.defaultLogFile), exitCode,
5235            ))
5236
5237        uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start))
5238        uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format(
5239            finish.strftime(TKS_PRINT_DATE_TIME_FORMAT),
5240            finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT),
5241        ))
5242        uLogger.debug("=-" * 50)
5243
5244        if not kwargs:
5245            sys.exit(exitCode)
5246
5247        else:
5248            return exitCode

Main function for work with TKSBrokerAPI in the console.

See examples: